├── .bzrignore ├── .coveragerc ├── .github └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── .readthedocs.yaml ├── AUTHORS ├── COPYING ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── clize ├── __init__.py ├── _sphinx.py ├── converters.py ├── errors.py ├── help.py ├── legacy.py ├── parameters.py ├── parser.py ├── runner.py ├── tests │ ├── __init__.py │ ├── test_converters.py │ ├── test_help.py │ ├── test_legacy.py │ ├── test_legacy_py3k.py │ ├── test_parameters.py │ ├── test_parser.py │ ├── test_runner.py │ ├── test_testutil.py │ ├── test_util.py │ └── util.py └── util.py ├── docs ├── Makefile ├── _static │ └── clize_style.css ├── _templates │ └── layout.html ├── alternatives.rst ├── api.rst ├── basics.rst ├── compositing.rst ├── conf.py ├── contributing.rst ├── dispatching.rst ├── docstring-reference.rst ├── faq.rst ├── history.rst ├── index.rst ├── interop.rst ├── json ├── make.bat ├── parser.rst ├── porting.rst ├── reference.rst ├── releases.rst └── why.rst ├── examples ├── README.rst ├── __init__.py ├── altcommands.py ├── argdeco.py ├── bfparam.py ├── deco_add_param.py ├── deco_provide_arg.py ├── echo.py ├── hello.py ├── helloworld.py ├── interop.py ├── logparam.py ├── mapped.py ├── multi.py ├── multicommands.py ├── naval.py └── typed_cli.py ├── pyproject.toml ├── setup.cfg ├── setup.py └── tox.ini /.bzrignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | ./build 3 | ./dist 4 | ./MANIFEST 5 | ./*.egg* 6 | ./.coverage 7 | ./htmlcov 8 | ./.tox 9 | ./venv* 10 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | data_file = coverage.coverage 3 | branch = True 4 | 5 | [report] 6 | exclude_lines = 7 | pragma: no cover 8 | 9 | raise AssertionError 10 | raise NotImplementedError 11 | if sys.version_info 12 | 13 | include = 14 | clize/* 15 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python package to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - v[0-9]* 7 | release: 8 | types: 9 | - published 10 | 11 | jobs: 12 | python-ci: 13 | uses: epsy/python-workflows/.github/workflows/python-cd.yaml@main 14 | secrets: 15 | TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} 16 | PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags: 8 | - '*' 9 | pull_request: 10 | types: 11 | - 'opened' 12 | - 'synchronize' 13 | workflow_dispatch: 14 | schedule: 15 | # Schedule every Saturday at 00:30 UTC 16 | - cron: '30 0 * * 6' 17 | 18 | jobs: 19 | python-ci: 20 | uses: epsy/python-workflows/.github/workflows/python-ci.yaml@main 21 | with: 22 | package-folder: clize 23 | python-windows-ci: 24 | name: "Run tests (Windows)" 25 | runs-on: 'windows-latest' 26 | continue-on-error: true 27 | steps: 28 | - uses: epsy/python-workflows/install-tox@main 29 | with: 30 | python-version: "3.10" 31 | - name: Test with tox 32 | uses: epsy/python-workflows/tox-ci@main 33 | with: 34 | tox-args: "" 35 | python-test-args: "-m unittest" 36 | - name: Verify that tox 'test' env ran 37 | run: cat ./tox-proof-test 38 | shell: bash 39 | mypy: 40 | name: "Run mypy on typed example" 41 | runs-on: 'ubuntu-latest' 42 | steps: 43 | - uses: epsy/python-workflows/install-tox@main 44 | with: 45 | python-version: "3.10" 46 | - name: Run mypy with tox 47 | uses: epsy/python-workflows/tox-ci@main 48 | with: 49 | tox-args: -e typecheck 50 | python-test-args: examples/typed_cli.py 51 | problem-matcher: mypy 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | /build 3 | /dist 4 | /MANIFEST 5 | /*.egg* 6 | /.coverage 7 | /coverage.* 8 | /htmlcov 9 | /.tox 10 | /venv* 11 | *.pyc 12 | _build 13 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-20.04 8 | tools: 9 | python: "3.9" 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | python: 15 | install: 16 | - method: pip 17 | path: . 18 | extra_requirements: 19 | - clize-own-docs 20 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Clize is written and maintained by: 2 | 3 | * Yann Kaiser 4 | 5 | Contributors 6 | ------------ 7 | 8 | * Alexandre Bonnetain 9 | * Chris Angelico 10 | * Étienne BERSAC 11 | * Francis T. O'Donovan 12 | * Karan Parikh 13 | * Kevin Samuel 14 | * Kian-Meng Ang 15 | * Konstantinos Koukopoulos 16 | * Michael Gielda 17 | * NODA Kai 18 | * Rasmus Scholer 19 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS for 3 | # details. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include examples *.py 2 | include README.rst 3 | include LICENSE.txt 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ***** 2 | Clize 3 | ***** 4 | 5 | .. image:: https://readthedocs.org/projects/clize/badge/?version=stable 6 | :target: http://clize.readthedocs.io/en/stable/?badge=stable 7 | :alt: Documentation Status 8 | .. image:: https://badges.gitter.im/Join%20Chat.svg 9 | :alt: Join the chat at https://gitter.im/epsy/clize 10 | :target: https://gitter.im/epsy/clize?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 11 | .. image:: https://github.com/epsy/clize/actions/workflows/ci.yml/badge.svg?branch=master 12 | :target: https://github.com/epsy/clize/actions/workflows/ci.yml 13 | .. image:: https://coveralls.io/repos/epsy/clize/badge.svg?branch=master 14 | :target: https://coveralls.io/r/epsy/clize?branch=master 15 | 16 | Clize is an argument parser for `Python `_. You can 17 | use Clize as an alternative to ``argparse`` if you want an even easier way to 18 | create command-line interfaces. 19 | 20 | **With Clize, you can:** 21 | 22 | * Create command-line interfaces by creating functions and passing them to 23 | `clize.run`. 24 | * Enjoy a CLI automatically created from your functions' parameters. 25 | * Bring your users familiar ``--help`` messages generated from your docstrings. 26 | * Reuse functionality across multiple commands using decorators. 27 | * Extend Clize with new parameter behavior. 28 | 29 | **Here's an example:** 30 | 31 | .. code-block:: python 32 | 33 | from clize import run 34 | 35 | def hello_world(name=None, *, no_capitalize=False): 36 | """Greets the world or the given name. 37 | 38 | :param name: If specified, only greet this person. 39 | :param no_capitalize: Don't capitalize the given name. 40 | """ 41 | if name: 42 | if not no_capitalize: 43 | name = name.title() 44 | return 'Hello {0}!'.format(name) 45 | return 'Hello world!' 46 | 47 | if __name__ == '__main__': 48 | run(hello_world) 49 | 50 | The python code above can now be used on the command-line as follows: 51 | 52 | .. code-block:: console 53 | 54 | $ pip install clize 55 | $ python3 hello.py --help 56 | Usage: hello.py [OPTIONS] name 57 | 58 | Greets the world or the given name. 59 | 60 | Positional arguments: 61 | name If specified, only greet this person. 62 | 63 | Options: 64 | --no-capitalize Don't capitalize the given name. 65 | 66 | Other actions: 67 | -h, --help Show the help 68 | $ python3 hello.py 69 | Hello world! 70 | $ python3 hello.py john 71 | Hello John! 72 | $ python3 hello.py dave --no-capitalize 73 | Hello dave! 74 | 75 | You can find the documentation and tutorials at http://clize.readthedocs.io/ 76 | -------------------------------------------------------------------------------- /clize/__init__.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | 5 | """procedurally generate command-line interfaces from callables""" 6 | 7 | from clize.parser import Parameter 8 | from clize.runner import Clize, SubcommandDispatcher, run 9 | from clize.legacy import clize, make_flag 10 | from clize.errors import UserError, ArgumentError 11 | 12 | __all__ = [ 13 | 'run', 'Parameter', 'UserError', 14 | 'Clize', 'ArgumentError', 'SubcommandDispatcher', 15 | 'clize', 'make_flag' 16 | ] 17 | -------------------------------------------------------------------------------- /clize/_sphinx.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | 5 | """Adds auto- and domain directives that don't clash the referencing system""" 6 | from sphinx.domains import python 7 | from sphinx.ext import autodoc 8 | 9 | 10 | class NoDupesObjectDirective(python.PyObject): 11 | def get_ref_context(self): 12 | try: 13 | return self.env.ref_context 14 | except AttributeError: 15 | return self.env.temp_data 16 | 17 | def add_target_and_index(self, name_cls, sig, signode): 18 | modname = self.options.get( 19 | 'module', self.get_ref_context().get('py:module')) 20 | fullname = (modname and modname + '.' or '') + name_cls[0] 21 | # note target 22 | if fullname not in self.state.document.ids: 23 | signode['names'].append(fullname) 24 | signode['ids'].append(fullname) 25 | signode['first'] = (not self.names) 26 | self.state.document.note_explicit_target(signode) 27 | 28 | indextext = self.get_index_text(modname, name_cls) 29 | if indextext: 30 | self.indexnode['entries'].append(('single', indextext, 31 | fullname, '', None)) 32 | 33 | def get_index_text(self, *args, **kwargs): 34 | back = self.objtype 35 | try: 36 | self.objtype = back[4:] 37 | return super(NoDupesObjectDirective, self 38 | ).get_index_text(*args, **kwargs) 39 | finally: 40 | self.objtype = back 41 | 42 | 43 | class MoreInfoDocumenter(autodoc.Documenter): 44 | def __init__(self, *args, **kwargs): 45 | super(MoreInfoDocumenter, self).__init__(*args, **kwargs) 46 | self.__fixed = False 47 | 48 | def add_directive_header(self, sig): 49 | if not self.__fixed: 50 | directive = getattr(self, 'directivetype', self.objtype) 51 | self.directivetype = 'more' + directive 52 | return super(MoreInfoDocumenter, self).add_directive_header(sig) 53 | 54 | def can_document_member(self, *args): 55 | return False 56 | 57 | 58 | class MoreInfoDirective(autodoc.AutoDirective): 59 | _registry = {} 60 | 61 | def run(self, *args, **kwargs): 62 | return super(MoreInfoDirective, self).run(*args, **kwargs) 63 | 64 | 65 | def add_moredoc(app, objtype): 66 | cls = autodoc.AutoDirective._registry[objtype] 67 | documenter = type('More'+cls.__name__, (MoreInfoDocumenter, cls), {}) 68 | autodoc.AutoDirective._registry['more' + objtype] = documenter 69 | app.add_directive('automore' + objtype, autodoc.AutoDirective) 70 | 71 | dirname = getattr(cls, 'directivetype', cls.objtype) 72 | dircls = python.PythonDomain.directives[dirname] 73 | directive = type("NoDupes"+dircls.__name__, 74 | (NoDupesObjectDirective, dircls), {}) 75 | python.PythonDomain.directives['more' + dirname] = directive 76 | #app.add_directive('py:more' + dirname, directive) 77 | 78 | 79 | def setup(app): 80 | for name in list(autodoc.AutoDirective._registry): 81 | add_moredoc(app, name) 82 | -------------------------------------------------------------------------------- /clize/converters.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | import contextlib 5 | import sys 6 | import io 7 | import os 8 | import warnings 9 | from functools import partial 10 | 11 | from sigtools.modifiers import autokwoargs 12 | 13 | from clize import parser, errors, util 14 | 15 | 16 | @parser.value_converter(name='TIME') 17 | def datetime(arg): 18 | """Parses a date into a `datetime` value 19 | 20 | Requires ``dateutil`` to be installed. 21 | """ 22 | from dateutil import parser as dparser 23 | 24 | return dparser.parse(arg) 25 | 26 | 27 | class _FileOpener(object): 28 | def __init__(self, arg, kwargs, stdio, keep_stdio_open): 29 | self.arg = arg 30 | self.kwargs = kwargs 31 | self.stdio = stdio 32 | self.keep_stdio_open = keep_stdio_open 33 | self.validate_permissions() 34 | 35 | def validate_permissions(self): 36 | mode = self.kwargs.get('mode', 'r') 37 | if self.arg == self.stdio: 38 | return 39 | exists = os.access(self.arg, os.F_OK) 40 | if not exists: 41 | if 'r' in mode and '+' not in mode: 42 | raise errors.CliValueError( 43 | 'File does not exist: {0!r}'.format(self.arg)) 44 | else: 45 | dirname = os.path.dirname(self.arg) 46 | if not dirname or os.access(dirname, os.W_OK): 47 | return 48 | if not os.path.exists(dirname): 49 | raise errors.CliValueError( 50 | 'Directory does not exist: {0!r}'.format(self.arg)) 51 | elif os.access(self.arg, os.W_OK): 52 | return 53 | raise errors.CliValueError( 54 | 'Permission denied: {0!r}'.format(self.arg)) 55 | 56 | def __enter__(self): 57 | if self.arg == self.stdio: 58 | mode = self.kwargs.get('mode', 'r') 59 | self.f = sys.stdin if 'r' in mode else sys.stdout 60 | else: 61 | try: 62 | self.f = io.open(self.arg, **self.kwargs) 63 | except IOError as exc: 64 | raise _convert_ioerror(self.arg, exc) 65 | return self.f 66 | 67 | def __exit__(self, *exc_info): 68 | if self.arg != self.stdio or not self.keep_stdio_open: 69 | self.f.close() 70 | 71 | 72 | @contextlib.contextmanager 73 | def _silence_convert_default_warning(): 74 | with warnings.catch_warnings(): 75 | warnings.filterwarnings("ignore", "The convert_default parameter of value_converter", DeprecationWarning, r"clize\..*") 76 | yield 77 | 78 | 79 | def _conversion_filter(arg): 80 | return isinstance(arg, str) 81 | 82 | 83 | with _silence_convert_default_warning(): 84 | @parser.value_converter(name='FILE', convert_default=True, convert_default_filter=_conversion_filter) 85 | @autokwoargs(exceptions=['arg']) 86 | def file(arg=util.UNSET, stdio='-', keep_stdio_open=False, **kwargs): 87 | """Takes a file argument and provides a Python object that opens a file 88 | 89 | :: 90 | 91 | def main(in_: file(), out: file(mode='w')): 92 | with in_ as infile, out as outfile: 93 | outfile.write(infile.read()) 94 | 95 | :param stdio: If this value is passed as argument, it will be interpreted 96 | as *stdin* or *stdout* depending on the ``mode`` parameter supplied. 97 | :param keep_stdio_open: If true, does not close the file if it is *stdin* 98 | or *stdout*. 99 | 100 | Other arguments will be relayed to `io.open`. 101 | 102 | You can specify a default file name using `clize.Parameter.cli_default`:: 103 | 104 | def main(inf: (file(), Parameter.cli_default("-"))): 105 | with inf as f: 106 | print(f) 107 | 108 | .. code-block:: console 109 | 110 | $ python3 ./main.py 111 | <_io.TextIOWrapper name='' mode='r' encoding='UTF-8'> 112 | 113 | 114 | """ 115 | if arg is not util.UNSET: 116 | return _FileOpener(arg, kwargs, stdio, keep_stdio_open) 117 | with _silence_convert_default_warning(): 118 | return parser.value_converter( 119 | partial(_FileOpener, kwargs=kwargs, 120 | stdio=stdio, keep_stdio_open=keep_stdio_open), 121 | name='FILE', convert_default=True, convert_default_filter=_conversion_filter) 122 | 123 | 124 | def _convert_ioerror(arg, exc): 125 | nexc = errors.ArgumentError('{0.strerror}: {1!r}'.format(exc, arg)) 126 | nexc.__cause__ = exc 127 | return nexc 128 | -------------------------------------------------------------------------------- /clize/errors.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | 5 | from functools import partial 6 | 7 | 8 | class UserError(ValueError): 9 | """An error to be printed to the user.""" 10 | 11 | def __str__(self): 12 | return self.prefix_with_pname(super(UserError, self).__str__()) 13 | 14 | def prefix_with_pname(self, message): 15 | return '{0}: {1}'.format(self.get_pname('Error'), message) 16 | 17 | def get_pname(self, default='command'): 18 | try: 19 | return self.pname 20 | except AttributeError: 21 | return default 22 | 23 | 24 | class ArgumentError(UserError): 25 | """An error related to the arguments passed through the command-line 26 | interface""" 27 | def __init__(self, message=None): 28 | if message is not None: 29 | self.message = message 30 | 31 | def __str__(self): 32 | usage = '' 33 | try: 34 | usage = '\n' + '\n'.join( 35 | self.cli.helper.show_usage(self.get_pname())) 36 | except Exception: 37 | pass 38 | return self.prefix_with_pname(self.message + usage) 39 | 40 | 41 | class MissingRequiredArguments(ArgumentError): 42 | """Raised when required parameters have not been provided an argument""" 43 | 44 | def __init__(self, missing): 45 | self.missing = missing 46 | 47 | @property 48 | def message(self): 49 | return "Missing required arguments: {0}".format( 50 | ', '.join(arg.display_name for arg in self.missing)) 51 | 52 | 53 | class TooManyArguments(ArgumentError): 54 | """Raised when too many positional arguments have been passed for the 55 | parameters to consume.""" 56 | 57 | def __init__(self, extra): 58 | self.extra = extra 59 | 60 | @property 61 | def message(self): 62 | return "Received extra arguments: {0}".format( 63 | ' '.join(self.extra)) 64 | 65 | 66 | class DuplicateNamedArgument(ArgumentError): 67 | """Raised when a named option or flag has been passed twice.""" 68 | 69 | @property 70 | def message(self): 71 | return "{0} was specified more than once".format( 72 | self.param.aliases[0]) 73 | 74 | 75 | class UnknownOption(ArgumentError): 76 | """Raised when a named argument has no matching parameter.""" 77 | 78 | def __init__(self, name): 79 | self.name = name 80 | 81 | @property 82 | def message(self): 83 | best_guess = None 84 | if self.ba: 85 | best_guess = self.ba.get_best_guess(self.name) 86 | if best_guess: 87 | return "Unknown option {0!r}. Did you mean {1!r}?" \ 88 | .format(self.name, best_guess) 89 | else: 90 | return "Unknown option {0!r}".format(self.name) 91 | 92 | 93 | class MissingValue(ArgumentError): 94 | """Raised when an option received no value.""" 95 | 96 | @property 97 | def message(self): 98 | return "No value found after {0}".format(self.param.display_name) 99 | 100 | 101 | class NotEnoughValues(ArgumentError): 102 | """Raised when MultiOptionParameter is given less values than its min 103 | parameter.""" 104 | 105 | @property 106 | def message(self): 107 | return "Received too few values for {0.display_name}".format( 108 | self.param) 109 | 110 | 111 | class TooManyValues(ArgumentError): 112 | """Raised when MultiOptionParameter is given more values than its max 113 | parameter.""" 114 | 115 | @property 116 | def message(self): 117 | return "Received too many values for {0.display_name}".format( 118 | self.param) 119 | 120 | 121 | class CliValueError(ValueError): 122 | """Specialization of `ValueError` for showing a message to the user along 123 | with the error rather than just the incorrect value.""" 124 | 125 | class BadArgumentFormat(ArgumentError): 126 | """Raised when an argument cannot be converted to the correct format.""" 127 | 128 | def __init__(self, text): 129 | self.text = text 130 | 131 | @property 132 | def message(self): 133 | return "Bad value for {0.display_name}: {1}".format( 134 | self.param, self.text) 135 | 136 | 137 | class ArgsBeforeAlternateCommand(ArgumentError): 138 | """Raised when there are arguments before a non-fallback alternate 139 | command.""" 140 | def __init__(self, param): 141 | self.param = param 142 | 143 | @property 144 | def message(self): 145 | return "Arguments found before alternate action parameter {0}".format( 146 | self.param.display_name) 147 | 148 | 149 | class SetErrorContext(object): 150 | """Context manager that sets attributes on exceptions that are raised 151 | past it""" 152 | 153 | def __init__(self, exc_type, **attributes): 154 | """ 155 | :param exc_type: The exception type to operate on. 156 | :param attributes: The attributes to set on the matching exceptions. 157 | They will only be set if yet unset on the exception. 158 | """ 159 | self.exc_type = exc_type 160 | self.values = attributes 161 | 162 | def __enter__(self): 163 | return self 164 | 165 | def __exit__(self, exc_type, exc_val, exc_tb): 166 | if isinstance(exc_val, self.exc_type): 167 | for key, val in self.values.items(): 168 | if not hasattr(exc_val, key): 169 | setattr(exc_val, key, val) 170 | 171 | SetUserErrorContext = partial(SetErrorContext, UserError) 172 | SetArgumentErrorContext = partial(SetErrorContext, ArgumentError) 173 | -------------------------------------------------------------------------------- /clize/legacy.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | 5 | import warnings 6 | from functools import partial 7 | from itertools import chain 8 | from collections import defaultdict 9 | 10 | from sigtools import modifiers, specifiers 11 | 12 | from clize import runner, parser, util, errors 13 | 14 | 15 | def _convert_coerce(func): 16 | if func not in parser._implicit_converters: 17 | func = parser.value_converter(func) 18 | return func 19 | 20 | def _clize(fn, alias={}, force_positional=(), coerce={}, 21 | require_excess=False, extra=(), 22 | use_kwoargs=None): 23 | sig = specifiers.signature(fn) 24 | has_kwoargs = False 25 | annotations = defaultdict(list) 26 | ann_positional = [] 27 | for param in sig.parameters.values(): 28 | coerce_set = False 29 | if param.kind == param.KEYWORD_ONLY: 30 | has_kwoargs = True 31 | elif param.kind == param.VAR_KEYWORD: 32 | annotations[param.name].append(parser.Parameter.IGNORE) 33 | elif require_excess and param.kind == param.VAR_POSITIONAL: 34 | annotations[param.name].append(parser.Parameter.REQUIRED) 35 | if param.annotation != param.empty: 36 | for thing in util.maybe_iter(param.annotation): 37 | if thing == clize.POSITIONAL: 38 | ann_positional.append(param.name) 39 | continue 40 | elif callable(thing): 41 | coerce_set = True 42 | thing = _convert_coerce(thing) 43 | annotations[param.name].append(thing) 44 | try: 45 | func = coerce[param.name] 46 | except KeyError: 47 | pass 48 | else: 49 | annotations[param.name].append(_convert_coerce(func)) 50 | coerce_set = True 51 | annotations[param.name].extend(alias.get(param.name, ())) 52 | if not coerce_set and param.default != param.empty: 53 | annotations[param.name].append( 54 | _convert_coerce(type(param.default))) 55 | fn = modifiers.annotate(**annotations)(fn) 56 | use_kwoargs = has_kwoargs if use_kwoargs is None else use_kwoargs 57 | if not use_kwoargs: 58 | fn = modifiers.autokwoargs( 59 | exceptions=chain(ann_positional, force_positional))(fn) 60 | return runner.Clize(fn, extra=extra) 61 | 62 | 63 | @specifiers.forwards_to(_clize, 1) 64 | def clize(fn=None, **kwargs): 65 | """Compatibility with clize<3.0 releases. Decorates a function in order 66 | to be passed to `clize.run`. See :ref:`porting-2`.""" 67 | warnings.warn('Use clize.Clize instead of clize.clize, or pass the ' 68 | 'function directly to run(), undecorated. See ' 69 | 'http://clize.readthedocs.io/en/3.1/' 70 | 'porting.html#porting-clize-decorator ' 71 | 'for more information.', 72 | DeprecationWarning, stacklevel=2) 73 | if fn is None: 74 | return partial(_clize, **kwargs) 75 | else: 76 | return _clize(fn, **kwargs) 77 | 78 | clize.kwo = partial(clize, use_kwoargs=True) 79 | clize.POSITIONAL = clize.P = parser.ParameterFlag('POSITIONAL', 80 | 'clize.legacy.clize') 81 | 82 | 83 | class MakeflagParameter(parser.NamedParameter): 84 | def __init__(self, takes_argument, **kwargs): 85 | super(MakeflagParameter, self).__init__(**kwargs) 86 | self.takes_argument = takes_argument 87 | 88 | def get_value(self, ba, i): 89 | assert self.takes_argument != 0 90 | arg = ba.in_args[i] 91 | if ( 92 | self.takes_argument == 1 93 | or arg[1] != '-' and len(arg) > 2 94 | or '=' in arg 95 | ): 96 | return super(MakeflagParameter, self).get_value(ba, i) 97 | args = ba.in_args[i+1:i+1+self.takes_argument] 98 | if len(args) != self.takes_argument: 99 | raise errors.NotEnoughValues 100 | ba.skip = self.takes_argument 101 | return ' '.join(args) 102 | 103 | class MakeflagFuncParameter(MakeflagParameter): 104 | """Parameter class that imitates those returned by Clize 2's `make_flag` 105 | when passed a callable for source. See :ref:`porting-2`.""" 106 | def __init__(self, func, **kwargs): 107 | super(MakeflagFuncParameter, self).__init__(**kwargs) 108 | self.func = func 109 | 110 | def noop(self, *args, **kwargs): 111 | pass 112 | 113 | def read_argument(self, ba, i): 114 | val = True 115 | ret = self.func(name=ba.name, command=ba.sig, 116 | val=val, params=ba.kwargs) 117 | if ret: 118 | ba.func = self.noop 119 | 120 | 121 | class MakeflagOptionParameter(MakeflagParameter, parser.OptionParameter): 122 | pass 123 | 124 | 125 | class MakeflagIntOptionParameter(MakeflagParameter, parser.IntOptionParameter): 126 | pass 127 | 128 | 129 | def make_flag(source, names, default=False, type=None, 130 | help='', takes_argument=0): 131 | """Compatibility with clize<3.0 releases. Creates a parameter instance. 132 | See :ref:`porting-2`.""" 133 | warnings.warn('clize.legacy.make_flag is deprecated. See ' 134 | 'http://clize.readthedocs.io/en/3.1/' 135 | 'porting.html#porting-make-flag', 136 | DeprecationWarning, stacklevel=2) 137 | kwargs = {} 138 | kwargs['aliases'] = [util.name_py2cli(alias, kw=True) 139 | for alias in names] 140 | if callable(source): 141 | return MakeflagFuncParameter(source, takes_argument=takes_argument, 142 | **kwargs) 143 | cls = MakeflagOptionParameter 144 | kwargs['argument_name'] = source 145 | kwargs['conv'] = type or parser.is_true 146 | if not takes_argument: 147 | return parser.FlagParameter(value=True, **kwargs) 148 | kwargs['default'] = default 149 | kwargs['takes_argument'] = takes_argument 150 | if takes_argument == 1 and type is int: 151 | cls = MakeflagIntOptionParameter 152 | return cls(**kwargs) 153 | -------------------------------------------------------------------------------- /clize/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | 5 | import sys 6 | import warnings 7 | 8 | 9 | __all__ = [] 10 | 11 | 12 | if sys.argv and "test" in sys.argv[0]: 13 | warnings.filterwarnings("default") 14 | warnings.filterwarnings("error", module="clize") 15 | warnings.filterwarnings("error", module=".*/clize/") 16 | -------------------------------------------------------------------------------- /clize/tests/test_converters.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | import unittest 5 | from datetime import datetime 6 | import tempfile 7 | import shutil 8 | import os 9 | import stat 10 | import sys 11 | from io import StringIO 12 | 13 | from sigtools import support, modifiers 14 | 15 | from clize import parser, errors, converters, Parameter 16 | from clize.tests.util import Fixtures, SignatureFixtures, Tests 17 | 18 | 19 | class ConverterRepTests(SignatureFixtures): 20 | def _test(self, conv, rep, *, make_signature): 21 | sig = make_signature('*, par: c', globals={'c': conv}) 22 | csig = parser.CliSignature.from_signature(sig) 23 | self.assertEqual(str(csig), rep) 24 | 25 | datetime = converters.datetime, '--par=TIME' 26 | file = converters.file(), '--par=FILE' 27 | 28 | 29 | class ConverterTests(SignatureFixtures): 30 | def _test(self, conv, inp, out, *, make_signature): 31 | sig = make_signature('*, par: c', globals={'c': conv}) 32 | csig = parser.CliSignature.from_signature(sig) 33 | ba = self.read_arguments(csig, ['--par', inp]) 34 | self.assertEqual(out, ba.kwargs['par']) 35 | 36 | dt_jan1 = ( 37 | converters.datetime, '2014-01-01 12:00', datetime(2014, 1, 1, 12, 0)) 38 | 39 | 40 | skip_if_windows = unittest.skipIf(sys.platform.startswith("win"), "Unsupported on Windows") 41 | 42 | 43 | class FileConverterTests(Tests): 44 | def setUp(self): 45 | self.temp = tempfile.mkdtemp() 46 | self.completed = False 47 | 48 | def tearDown(self): 49 | def set_writable_and_retry(func, path, excinfo): 50 | os.chmod(path, stat.S_IWUSR) 51 | func(path) 52 | shutil.rmtree(self.temp, set_writable_and_retry) 53 | 54 | def run_conv(self, conv, path): 55 | sig = support.s('*, par: c', globals={'c': conv}) 56 | csig = parser.CliSignature.from_signature(sig) 57 | ba = self.read_arguments(csig, ['--par', path]) 58 | return ba.kwargs['par'] 59 | 60 | def test_ret_type(self): 61 | path = os.path.join(self.temp, 'afile') 62 | arg = self.run_conv(converters.file(mode='w'), path) 63 | self.assertTrue(isinstance(arg, converters._FileOpener)) 64 | type(arg).__enter__ 65 | 66 | def test_file_read(self): 67 | path = os.path.join(self.temp, 'afile') 68 | open(path, 'w').close() 69 | @modifiers.annotate(afile=converters.file()) 70 | def func(afile): 71 | with afile as f: 72 | self.assertEqual(f.name, path) 73 | self.assertEqual(f.mode, 'r') 74 | self.assertTrue(f.closed) 75 | self.completed = True 76 | o, e = self.crun(func, ['test', path]) 77 | self.assertFalse(o.getvalue()) 78 | self.assertFalse(e.getvalue()) 79 | self.assertTrue(self.completed) 80 | 81 | def test_not_called(self): 82 | path = os.path.join(self.temp, 'afile') 83 | open(path, 'w').close() 84 | @modifiers.annotate(afile=converters.file) 85 | def func(afile): 86 | with afile as f: 87 | self.assertEqual(f.name, path) 88 | self.assertEqual(f.mode, 'r') 89 | self.assertTrue(f.closed) 90 | self.completed = True 91 | o, e = self.crun(func, ['test', path]) 92 | self.assertFalse(o.getvalue()) 93 | self.assertFalse(e.getvalue()) 94 | self.assertTrue(self.completed) 95 | 96 | def test_file_write(self): 97 | path = os.path.join(self.temp, 'afile') 98 | @modifiers.annotate(afile=converters.file(mode='w')) 99 | def func(afile): 100 | self.assertFalse(os.path.exists(path)) 101 | with afile as f: 102 | self.assertEqual(f.name, path) 103 | self.assertEqual(f.mode, 'w') 104 | self.assertTrue(f.closed) 105 | self.assertTrue(os.path.exists(path)) 106 | self.completed = True 107 | o, e = self.crun(func, ['test', path]) 108 | self.assertFalse(o.getvalue()) 109 | self.assertFalse(e.getvalue()) 110 | self.assertTrue(self.completed) 111 | 112 | def test_file_missing(self): 113 | path = os.path.join(self.temp, 'afile') 114 | self.assertRaises(errors.BadArgumentFormat, 115 | self.run_conv, converters.file(), path) 116 | @modifiers.annotate(afile=converters.file()) 117 | def func(afile): 118 | raise NotImplementedError 119 | stdout, stderr = self.crun(func, ['test', path]) 120 | self.assertFalse(stdout.getvalue()) 121 | self.assertTrue(stderr.getvalue().startswith( 122 | 'test: Bad value for afile: File does not exist: ')) 123 | 124 | def test_dir_missing(self): 125 | path = os.path.join(self.temp, 'adir/afile') 126 | self.assertRaises(errors.BadArgumentFormat, 127 | self.run_conv, converters.file(mode='w'), path) 128 | @modifiers.annotate(afile=converters.file(mode='w')) 129 | def func(afile): 130 | raise NotImplementedError 131 | stdout, stderr = self.crun(func, ['test', path]) 132 | self.assertFalse(stdout.getvalue()) 133 | self.assertTrue(stderr.getvalue().startswith( 134 | 'test: Bad value for afile: Directory does not exist: ')) 135 | 136 | def test_current_dir(self): 137 | path = 'afile' 138 | @modifiers.annotate(afile=converters.file(mode='w')) 139 | def func(afile): 140 | with afile as f: 141 | self.assertEqual(f.name, path) 142 | self.assertEqual(f.mode, 'w') 143 | self.assertTrue(f.closed) 144 | self.assertTrue(os.path.exists(path)) 145 | self.completed = True 146 | with self.cd(self.temp): 147 | stdout, stderr = self.crun(func, ['test', path]) 148 | self.assertFalse(stdout.getvalue()) 149 | self.assertFalse(stderr.getvalue()) 150 | self.assertTrue(self.completed) 151 | 152 | def test_deprecated_default_value(self): 153 | path = os.path.join(self.temp, 'default') 154 | open(path, 'w').close() 155 | def func(afile: converters.file()=path): 156 | with afile as f: 157 | self.assertEqual(f.name, path) 158 | self.assertEqual(f.mode, 'r') 159 | self.assertTrue(f.closed) 160 | self.completed = True 161 | with self.assertWarns(DeprecationWarning): 162 | stdout, stderr = self.crun(func, ['test']) 163 | self.assertFalse(stdout.getvalue()) 164 | self.assertFalse(stderr.getvalue()) 165 | self.assertTrue(self.completed) 166 | 167 | def test_cli_default_value(self): 168 | path = os.path.join(self.temp, 'default') 169 | open(path, 'w').close() 170 | def func(afile: (converters.file(), Parameter.cli_default(path))): 171 | with afile as f: 172 | self.assertEqual(f.name, path) 173 | self.assertEqual(f.mode, 'r') 174 | self.assertTrue(f.closed) 175 | self.completed = True 176 | stdout, stderr = self.crun(func, ['test']) 177 | self.assertFalse(stdout.getvalue()) 178 | self.assertFalse(stderr.getvalue()) 179 | self.assertTrue(self.completed) 180 | 181 | def test_default_none_value(self): 182 | def func(afile: converters.file() = None): 183 | self.assertIs(afile, None) 184 | self.completed = True 185 | stdout, stderr = self.crun(func, ['test']) 186 | self.assertFalse(stdout.getvalue()) 187 | self.assertFalse(stderr.getvalue()) 188 | self.assertTrue(self.completed) 189 | 190 | def test_noperm_file_write(self): 191 | path = os.path.join(self.temp, 'afile') 192 | open(path, mode='w').close() 193 | os.chmod(path, stat.S_IRUSR) 194 | self.assertRaises(errors.BadArgumentFormat, 195 | self.run_conv, converters.file(mode='w'), path) 196 | 197 | @skip_if_windows 198 | def test_noperm_dir(self): 199 | dpath = os.path.join(self.temp, 'adir') 200 | path = os.path.join(self.temp, 'adir/afile') 201 | os.mkdir(dpath) 202 | os.chmod(dpath, stat.S_IRUSR) 203 | self.assertRaises(errors.BadArgumentFormat, 204 | self.run_conv, converters.file(mode='w'), path) 205 | 206 | def test_race(self): 207 | path = os.path.join(self.temp, 'afile') 208 | open(path, mode='w').close() 209 | @modifiers.annotate(afile=converters.file(mode='w')) 210 | def func(afile): 211 | os.chmod(path, stat.S_IRUSR) 212 | with afile: 213 | raise NotImplementedError 214 | stdout, stderr = self.crun(func, ['test', path]) 215 | self.assertFalse(stdout.getvalue()) 216 | self.assertTrue(stderr.getvalue().startswith( 217 | 'test: Permission denied: ')) 218 | 219 | def test_stdin(self): 220 | stdin = StringIO() 221 | @modifiers.annotate(afile=converters.file()) 222 | def func(afile): 223 | with afile as f: 224 | self.assertIs(f, stdin) 225 | stdout, stderr = self.crun(func, ['test', '-'], stdin=stdin) 226 | self.assertTrue(stdin.closed) 227 | self.assertFalse(stdout.getvalue()) 228 | self.assertFalse(stderr.getvalue()) 229 | 230 | def test_stdout(self): 231 | @modifiers.annotate(afile=converters.file(mode='w')) 232 | def func(afile): 233 | with afile as f: 234 | self.assertIs(f, sys.stdout) 235 | stdout, stderr = self.crun(func, ['test', '-']) 236 | self.assertTrue(stdout.closed) 237 | self.assertFalse(stderr.getvalue()) 238 | 239 | def test_change_sym(self): 240 | @modifiers.annotate(afile=converters.file(stdio='gimmestdio')) 241 | def func(afile): 242 | with afile as f: 243 | self.assertIs(f, sys.stdin) 244 | stdout, stderr = self.crun(func, ['test', 'gimmestdio']) 245 | self.assertFalse(stdout.getvalue()) 246 | self.assertFalse(stderr.getvalue()) 247 | with self.cd(self.temp): 248 | self.assertFalse(os.path.exists('-')) 249 | stdout, stderr = self.crun(func, ['test', '-']) 250 | self.assertFalse(stdout.getvalue()) 251 | self.assertTrue(stderr.getvalue().startswith( 252 | 'test: Bad value for afile: File does not exist: ')) 253 | 254 | def test_no_sym(self): 255 | @modifiers.annotate(afile=converters.file(stdio=None)) 256 | def func(afile): 257 | raise NotImplementedError 258 | self.assertFalse(os.path.exists('-')) 259 | stdout, stderr = self.crun(func, ['test', '-']) 260 | self.assertFalse(stdout.getvalue()) 261 | self.assertTrue(stderr.getvalue().startswith( 262 | 'test: Bad value for afile: File does not exist: ')) 263 | 264 | def test_stdin_no_close(self): 265 | stdin = StringIO() 266 | @modifiers.annotate(afile=converters.file(keep_stdio_open=True)) 267 | def func(afile): 268 | with afile as f: 269 | self.assertIs(f, stdin) 270 | stdout, stderr = self.crun(func, ['test', '-'], stdin=stdin) 271 | self.assertFalse(stdin.closed) 272 | self.assertFalse(stdout.getvalue()) 273 | self.assertFalse(stderr.getvalue()) 274 | 275 | def test_stdout_no_close(self): 276 | @modifiers.annotate(afile=converters.file(mode='w', keep_stdio_open=True)) 277 | def func(afile): 278 | with afile as f: 279 | self.assertIs(f, sys.stdout) 280 | stdout, stderr = self.crun(func, ['test', '-']) 281 | self.assertFalse(stdout.closed) 282 | self.assertFalse(stdout.getvalue()) 283 | self.assertFalse(stderr.getvalue()) 284 | 285 | 286 | class ConverterErrorTests(Fixtures): 287 | def _test(self, conv, inp): 288 | sig = support.s('*, par: c', globals={'c': conv}) 289 | csig = parser.CliSignature.from_signature(sig) 290 | self.assertRaises(errors.BadArgumentFormat, 291 | self.read_arguments, csig, ['--par', inp]) 292 | 293 | dt_baddate = converters.datetime, 'not a date' 294 | -------------------------------------------------------------------------------- /clize/tests/test_legacy.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | 5 | import sys 6 | import unittest 7 | import warnings 8 | from io import StringIO 9 | 10 | from clize import clize, errors, runner, make_flag 11 | 12 | 13 | class OldInterfaceTests(unittest.TestCase): 14 | @classmethod 15 | def setUpClass(self): 16 | warnings.filterwarnings('ignore', '.*clize', DeprecationWarning) 17 | 18 | @classmethod 19 | def tearDownClass(self): 20 | warnings.filters.pop(0) 21 | 22 | class ParamTests(OldInterfaceTests): 23 | def test_pos(self): 24 | @clize 25 | def fn(one, two, three): 26 | return one, two, three 27 | self.assertEqual( 28 | fn('fn', "1", "2", "3"), 29 | ('1', '2', '3') 30 | ) 31 | 32 | def test_kwargs(self): 33 | @clize 34 | def fn(one='1', two='2', three='3'): 35 | return one, two, three 36 | self.assertEqual( 37 | fn('fn', '--three=6', '--two', '4'), 38 | ('1', '4', '6') 39 | ) 40 | 41 | def test_mixed(self): 42 | @clize 43 | def fn(one, two='2', three='3'): 44 | return one, two, three 45 | self.assertEqual( 46 | fn('fn', '--two', '4', '0'), 47 | ('0', '4', '3') 48 | ) 49 | 50 | def test_catchall(self): 51 | @clize 52 | def fn(one, two='2', *rest): 53 | return one, two, rest 54 | self.assertEqual( 55 | fn('fn', '--two=4', '1', '2', '3', '4'), 56 | ('1', '4', ('2', '3', '4')) 57 | ) 58 | 59 | def test_coerce(self): 60 | @clize 61 | def fn(one=1, two=2, three=False): 62 | return one, two, three 63 | self.assertEqual( 64 | fn('fn', '--one=0', '--two', '4', '--three'), 65 | (0, 4, True) 66 | ) 67 | 68 | def test_explicit_coerce(self): 69 | @clize(coerce={'one': int, 'two': int}) 70 | def fn(one, two): 71 | return one, two 72 | self.assertEqual( 73 | fn('fn', '1', '2'), 74 | (1, 2) 75 | ) 76 | 77 | def test_extra(self): 78 | @clize 79 | def fn(*args): 80 | return args 81 | self.assertEqual(fn('fn'), ()) 82 | self.assertEqual(fn('fn', '1'), ('1',)) 83 | self.assertEqual(fn('fn', '1', '2'), ('1', '2')) 84 | 85 | def test_extra_required(self): 86 | @clize(require_excess=True) 87 | def fn(*args): 88 | return args 89 | self.assertRaises(errors.MissingRequiredArguments, fn, 'fn') 90 | self.assertEqual(fn('fn', '1'), ('1',)) 91 | self.assertEqual(fn('fn', '1', '2'), ('1', '2')) 92 | 93 | def test_too_short(self): 94 | @clize 95 | def fn(one, two): 96 | raise NotImplementedError 97 | self.assertRaises(errors.MissingRequiredArguments, fn, 'fn', 'one') 98 | 99 | def test_too_long(self): 100 | @clize 101 | def fn(one, two): 102 | raise NotImplementedError 103 | self.assertRaises(errors.TooManyArguments, fn, 'fn', 'one', 'two', 'three') 104 | 105 | def test_missing_arg(self): 106 | @clize 107 | def fn(one='1', two='2'): 108 | raise NotImplementedError 109 | self.assertRaises(errors.MissingValue, fn, 'fn', '--one') 110 | 111 | def test_short_param(self): 112 | @clize(alias={'one': ('o',)}) 113 | def fn(one='1'): 114 | return one 115 | self.assertEqual(fn('fn', '--one', '0'), '0') 116 | self.assertEqual(fn('fn', '-o', '0'), '0') 117 | 118 | def test_short_int_param(self): 119 | @clize(alias={'one': ('o',), 'two': ('t',), 'three': ('s',)}) 120 | def fn(one=1, two=2, three=False): 121 | return one, two, three 122 | self.assertEqual(fn('fn', '--one', '0'), (0, 2, False)) 123 | self.assertEqual(fn('fn', '-o', '0', '-t', '4', '-s'), (0, 4, True)) 124 | self.assertEqual(fn('fn', '-o0t4s'), (0, 4, True)) 125 | 126 | def test_force_posarg(self): 127 | @clize(force_positional=('one',)) 128 | def fn(one=1): 129 | return one 130 | self.assertEqual(fn('fn', '0'), 0) 131 | 132 | def test_unknown_option(self): 133 | @clize 134 | def fn(one=1): 135 | raise NotImplementedError 136 | self.assertRaises(errors.UnknownOption, fn, 'fn', '--doesnotexist') 137 | 138 | def test_coerce_fail(self): 139 | @clize 140 | def fn(one=1): 141 | raise NotImplementedError 142 | self.assertRaises(errors.BadArgumentFormat, fn, 'fn', '--one=nan') 143 | 144 | def test_custom_coerc(self): 145 | def coerc(arg): 146 | return 42 147 | @clize(coerce={'one': coerc}) 148 | def fn(one): 149 | return one 150 | self.assertEqual(fn('fn', 'spam'), 42) 151 | 152 | def test_custom_type_default(self): 153 | class FancyDefault(object): 154 | def __init__(self, arg): 155 | self.arg = arg 156 | @clize 157 | def fn(one=FancyDefault('ham')): 158 | return one 159 | ret = fn('fn') 160 | self.assertEqual(type(ret), FancyDefault) 161 | self.assertEqual(ret.arg, 'ham') 162 | ret = fn('fn', '--one=spam') 163 | self.assertEqual(type(ret), FancyDefault) 164 | self.assertEqual(ret.arg, 'spam') 165 | 166 | def test_ignore_kwargs(self): 167 | @clize 168 | def fn(abc, xyz=0, **kwargs): 169 | return abc, xyz, kwargs 170 | abc, xyz, kwargs = fn('fn', 'abc') 171 | self.assertEqual(abc, 'abc') 172 | self.assertEqual(xyz, 0) 173 | self.assertEqual(kwargs, {}) 174 | 175 | def run_group(functions, args): 176 | disp = runner.SubcommandDispatcher(functions) 177 | return disp.cli(*args) 178 | 179 | class SubcommandTests(OldInterfaceTests): 180 | def test_pos(self): 181 | @clize 182 | def fn1(one, two): 183 | return one, two 184 | self.assertEqual( 185 | run_group((fn1,), ('group', 'fn1', 'one', 'two')), 186 | ('one', 'two') 187 | ) 188 | 189 | def test_opt(self): 190 | @clize 191 | def fn1(one='1', two='2'): 192 | return one, two 193 | self.assertEqual( 194 | run_group((fn1,), ('group', 'fn1', '--one=one', '--two', 'two')), 195 | ('one', 'two') 196 | ) 197 | 198 | def test_unknown_command(self): 199 | @clize 200 | def fn1(): 201 | raise NotImplementedError 202 | self.assertRaises( 203 | errors.ArgumentError, 204 | run_group, (fn1,), ('group', 'unknown') 205 | ) 206 | 207 | def test_no_command(self): 208 | @clize 209 | def fn1(): 210 | raise NotImplementedError 211 | self.assertRaises( 212 | errors.ArgumentError, 213 | run_group, (fn1,), ('group',) 214 | ) 215 | 216 | def test_opts_but_no_command(self): 217 | @clize 218 | def fn1(): 219 | raise NotImplementedError 220 | self.assertRaises( 221 | errors.ArgumentError, 222 | run_group, (fn1,), ('group', '--opt') 223 | ) 224 | 225 | class MakeFlagTests(OldInterfaceTests): 226 | def run_cli(self, func, args): 227 | orig_out = sys.stdout 228 | orig_err = sys.stderr 229 | try: 230 | sys.stdout = out = StringIO() 231 | sys.stderr = err = StringIO() 232 | ret = func(*args) 233 | finally: 234 | sys.stdout = orig_out 235 | sys.stderr = orig_err 236 | return ret, out, err 237 | 238 | def test_version(self): 239 | def show_version(name, **kwargs): 240 | print('this is the version') 241 | return True 242 | 243 | @clize( 244 | extra=( 245 | make_flag( 246 | source=show_version, 247 | names=('version', 'v'), 248 | help="Show the version", 249 | ), 250 | ) 251 | ) 252 | def fn(): 253 | raise NotImplementedError 254 | ret, out, err = self.run_cli(fn, ['test', '--version']) 255 | self.assertEqual(out.getvalue(), 'this is the version\n') 256 | self.assertEqual(err.getvalue(), '') 257 | ret, out, err = self.run_cli(fn, ['test', '-v']) 258 | self.assertEqual(out.getvalue(), 'this is the version\n') 259 | self.assertEqual(err.getvalue(), '') 260 | 261 | def test_keepgoing(self): 262 | def extra(name, command, val, params): 263 | if check_xyz: 264 | self.assertEqual(params['xyz'], 'xyz') 265 | else: 266 | self.assertFalse('xyz' in params) 267 | params['added'] = 'added' 268 | 269 | @clize( 270 | extra=( 271 | make_flag( 272 | source=extra, 273 | names=('extra',), 274 | ), 275 | ) 276 | ) 277 | def fn(arg1, arg2, added='', xyz=''): 278 | self.assertEqual(arg1, 'arg1') 279 | self.assertEqual(arg2, 'arg2') 280 | self.assertEqual(xyz, 'xyz') 281 | self.assertEqual(added, 'added') 282 | check_xyz = True 283 | self.run_cli(fn, ['test', 'arg1', '--xyz', 'xyz', 'arg2', '--extra']) 284 | check_xyz = False 285 | ret, out, err = self.run_cli( 286 | fn, ['test', 'arg1', '--extra', 'arg2', '--xyz', 'xyz']) 287 | 288 | def test_flag(self): 289 | @clize( 290 | extra=( 291 | make_flag( 292 | source='extra', 293 | names=('extra',) 294 | ), 295 | ) 296 | ) 297 | def fn(arg1, arg2, **kwargs): 298 | return arg1, arg2, kwargs 299 | arg1, arg2, kwargs = fn('test', 'arg1', '--extra', 'arg2') 300 | self.assertEqual(arg1, 'arg1') 301 | self.assertEqual(arg2, 'arg2') 302 | self.assertEqual(kwargs, {'extra': True}) 303 | 304 | def test_opt(self): 305 | @clize( 306 | extra=( 307 | make_flag( 308 | source='extra', 309 | names=('extra',), 310 | type=str, 311 | takes_argument=1 312 | ), 313 | ) 314 | ) 315 | def fn(arg1, arg2, **kwargs): 316 | return arg1, arg2, kwargs 317 | arg1, arg2, kwargs = fn('test', 'arg1', '--extra', 'extra', 'arg2') 318 | self.assertEqual(arg1, 'arg1') 319 | self.assertEqual(arg2, 'arg2') 320 | self.assertEqual(kwargs, {'extra': 'extra'}) 321 | 322 | def test_intopt(self): 323 | @clize( 324 | extra=( 325 | make_flag( 326 | source='extra', 327 | names=('extra', 'e'), 328 | type=int, 329 | takes_argument=1 330 | ), 331 | ) 332 | ) 333 | def fn(arg1, arg2, **kwargs): 334 | return arg1, arg2, kwargs 335 | arg1, arg2, kwargs = fn('test', 'arg1', '--extra', '42', 'arg2') 336 | self.assertEqual(arg1, 'arg1') 337 | self.assertEqual(arg2, 'arg2') 338 | self.assertEqual(kwargs, {'extra': 42}) 339 | arg1, arg2, kwargs = fn('test', 'arg1', '-e42', 'arg2') 340 | self.assertEqual(arg1, 'arg1') 341 | self.assertEqual(arg2, 'arg2') 342 | self.assertEqual(kwargs, {'extra': 42}) 343 | 344 | def test_moreargs(self): 345 | @clize( 346 | extra=( 347 | make_flag( 348 | source='extra', 349 | names=('extra', 'e'), 350 | type=str, 351 | takes_argument=3 352 | ), 353 | ) 354 | ) 355 | def fn(arg1, arg2, **kwargs): 356 | return arg1, arg2, kwargs 357 | arg1, arg2, kwargs = fn( 358 | 'test', 'arg1', '--extra', 'extra1', 'extra2', 'extra3', 'arg2') 359 | self.assertEqual(arg1, 'arg1') 360 | self.assertEqual(arg2, 'arg2') 361 | self.assertEqual(kwargs, {'extra': 'extra1 extra2 extra3'}) 362 | arg1, arg2, kwargs = fn( 363 | 'test', 'arg1', '-e', 'extra1', 'extra2', 'extra3', 'arg2') 364 | self.assertEqual(arg1, 'arg1') 365 | self.assertEqual(arg2, 'arg2') 366 | self.assertEqual(kwargs, {'extra': 'extra1 extra2 extra3'}) 367 | arg1, arg2, kwargs = fn('test', 'arg1', '--extra=extra', 'arg2') 368 | self.assertEqual(arg1, 'arg1') 369 | self.assertEqual(arg2, 'arg2') 370 | self.assertEqual(kwargs, {'extra': 'extra'}) 371 | arg1, arg2, kwargs = fn('test', 'arg1', '-eextra', 'arg2') 372 | self.assertEqual(arg1, 'arg1') 373 | self.assertEqual(arg2, 'arg2') 374 | self.assertEqual(kwargs, {'extra': 'extra'}) 375 | self.assertRaises(errors.NotEnoughValues, 376 | fn, 'test', 'arg1', 'arg2', '--extra', 'extra') 377 | 378 | def test_intopt_moreargs(self): 379 | @clize( 380 | extra=( 381 | make_flag( 382 | source='extra', 383 | names=('extra', 'e'), 384 | type=int, 385 | takes_argument=2 386 | ), 387 | ) 388 | ) 389 | def fn(arg1, arg2, **kwargs): 390 | return arg1, arg2, kwargs 391 | arg1, arg2, kwargs = fn('test', 'arg1', '-e42', 'arg2') 392 | self.assertEqual(arg1, 'arg1') 393 | self.assertEqual(arg2, 'arg2') 394 | self.assertEqual(kwargs, {'extra': 42}) 395 | arg1, arg2, kwargs = fn('test', 'arg1', '--extra=42', 'arg2') 396 | self.assertEqual(arg1, 'arg1') 397 | self.assertEqual(arg2, 'arg2') 398 | self.assertEqual(kwargs, {'extra': 42}) 399 | self.assertRaises(errors.BadArgumentFormat, 400 | fn, 'test', '-e', 'extra1', 'extra2') 401 | self.assertRaises(errors.BadArgumentFormat, 402 | fn, 'test', '--extra', 'extra1', 'extra2') 403 | -------------------------------------------------------------------------------- /clize/tests/test_legacy_py3k.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | 5 | from sigtools import modifiers 6 | from clize import clize, errors 7 | 8 | from clize.tests.test_legacy import OldInterfaceTests 9 | 10 | #from tests import HelpTester 11 | 12 | class AnnotationParams(OldInterfaceTests): 13 | def test_alias(self): 14 | @clize 15 | @modifiers.annotate(one='o') 16 | def fn(one=1): 17 | return one 18 | self.assertEqual( 19 | fn('fn', '-o', '2'), 20 | 2 21 | ) 22 | 23 | def test_position(self): 24 | @clize 25 | @modifiers.annotate(one=clize.POSITIONAL) 26 | def fn(one=1): 27 | return one 28 | self.assertEqual( 29 | fn('fn', '2'), 30 | 2 31 | ) 32 | 33 | def test_coerce(self): 34 | @clize 35 | @modifiers.annotate(one=float) 36 | def fn(one): 37 | return one 38 | self.assertEqual( 39 | fn('fn', '2.1'), 40 | 2.1 41 | ) 42 | 43 | def test_coerce_and_default(self): 44 | @clize 45 | @modifiers.annotate(one=float) 46 | def fn(one=1): 47 | return one 48 | self.assertEqual( 49 | fn('fn'), 50 | 1 51 | ) 52 | self.assertEqual( 53 | fn('fn', '--one', '2.1'), 54 | 2.1 55 | ) 56 | 57 | def test_multiple(self): 58 | @clize 59 | @modifiers.annotate(one=(float, clize.POSITIONAL)) 60 | def fn(one=1): 61 | return one 62 | self.assertEqual( 63 | fn('fn', '2.1'), 64 | 2.1 65 | ) 66 | self.assertEqual( 67 | fn('fn'), 68 | 1 69 | ) 70 | 71 | class AnnotationFailures(OldInterfaceTests): 72 | def test_coerce_twice(self): 73 | def test(): 74 | @clize 75 | @modifiers.annotate(one=(float, int)) 76 | def fn(one): 77 | raise NotImplementedError 78 | fn.signature 79 | self.assertRaises(ValueError, test) 80 | 81 | def test_alias_space(self): 82 | def test(): 83 | @clize 84 | @modifiers.annotate(one='a b') 85 | def fn(one=1): 86 | raise NotImplementedError 87 | fn.signature 88 | self.assertRaises(ValueError, test) 89 | 90 | def test_unknown(self): 91 | def test(): 92 | @clize 93 | @modifiers.annotate(one=1.0) 94 | def fn(one): 95 | raise NotImplementedError 96 | fn.signature 97 | self.assertRaises(ValueError, test) 98 | 99 | class KwoargsParams(OldInterfaceTests): 100 | def test_kwoparam(self): 101 | @clize 102 | @modifiers.kwoargs('one') 103 | def fn(one): 104 | return one 105 | 106 | self.assertEqual( 107 | fn('fn', '--one=one'), 108 | 'one' 109 | ) 110 | 111 | def test_kwoparam_required(self): 112 | @clize 113 | @modifiers.kwoargs('one') 114 | def fn(one): 115 | raise NotImplementedError 116 | 117 | self.assertRaises(errors.MissingRequiredArguments, fn, 'fn') 118 | 119 | def test_kwoparam_optional(self): 120 | @clize 121 | @modifiers.kwoargs('one') 122 | def fn(one=1): 123 | return one 124 | self.assertEqual( 125 | fn('fn'), 126 | 1 127 | ) 128 | self.assertEqual( 129 | fn('fn', '--one', '2'), 130 | 2 131 | ) 132 | self.assertEqual( 133 | fn('fn', '--one=2'), 134 | 2 135 | ) 136 | 137 | def test_optional_pos(self): 138 | @clize.kwo 139 | def fn(one, two=2): 140 | return one, two 141 | self.assertEqual( 142 | fn('fn', '1'), 143 | ('1', 2) 144 | ) 145 | self.assertEqual( 146 | fn('fn', '1', '3'), 147 | ('1', 3) 148 | ) 149 | -------------------------------------------------------------------------------- /clize/tests/test_testutil.py: -------------------------------------------------------------------------------- 1 | from clize.tests import util 2 | 3 | 4 | class AssertLinesEqualTests(util.Fixtures): 5 | def _test(self, match, exp, real): 6 | if match: 7 | self.assertLinesEqual(exp, real) 8 | else: 9 | with self.assertRaises(AssertionError): 10 | self.assertLinesEqual(exp, real) 11 | 12 | 13 | empty = True, "", "" 14 | one_line = True, "message", "message" 15 | trailing_space = True, "message", "message " 16 | trailing_newline = True, "message\n", "message\n" 17 | 18 | indent = True, """ 19 | This is fine 20 | Subindent 21 | """, "This is fine\n Subindent" 22 | 23 | one_different_line = False, "message", "telegram" 24 | differennt_indent = False, """ 25 | This is fine 26 | This isn't indented 27 | """, "This is fine\n This isn't indented" 28 | -------------------------------------------------------------------------------- /clize/tests/test_util.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | 5 | from clize import util 6 | from clize.tests.util import Fixtures 7 | 8 | 9 | def formatter(**kwargs): 10 | kwargs.setdefault('max_width', 50) 11 | return util.Formatter(**kwargs) 12 | 13 | 14 | def equal(s): 15 | def _deco(func): 16 | return func, s 17 | return _deco 18 | 19 | 20 | lorem = ( 21 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam finibus ' 22 | 'erat mi, eget accumsan metus.') 23 | slorem = 'Lorem ipsum dolor sit amet' 24 | 25 | 26 | class FormatterTests(Fixtures): 27 | def _test(self, func, expected_ret): 28 | f = formatter(max_width=50) 29 | ret = func(self, f) 30 | if ret is None: 31 | ret = f 32 | self.assertEqual(expected_ret, str(f)) 33 | 34 | @equal('') 35 | def empty(self, f): 36 | pass 37 | 38 | @equal('hello') 39 | def line(self, f): 40 | f.append('hello') 41 | 42 | @equal('hello\nworld') 43 | def lines(self, f): 44 | f.append('hello') 45 | f.append('world') 46 | 47 | @equal('hello\n\nworld') 48 | def paragraph(self, f): 49 | f.append('hello') 50 | f.new_paragraph() 51 | f.append('world') 52 | 53 | @equal('hello\n\nworld') 54 | def paragraph_dupe(self, f): 55 | f.append('hello') 56 | f.new_paragraph() 57 | f.new_paragraph() 58 | f.append('world') 59 | 60 | @equal('hello\nworld') 61 | def extend(self, f): 62 | f.extend(['hello', 'world']) 63 | 64 | @equal('') 65 | def extend_empty(self, f): 66 | f.extend([]) 67 | 68 | @equal('hello\n\nworld') 69 | def extend_ewp(self, f): 70 | f.append('hello') 71 | f.new_paragraph() 72 | f.extend(['', 'world']) 73 | 74 | @equal('hello\nworld') 75 | def extend_f(self, f): 76 | f1 = formatter() 77 | f1.append('hello') 78 | f1.append('world') 79 | f.extend(f1) 80 | 81 | @equal('hello\n\nworld') 82 | def extend_f_newp(self, f): 83 | f.append('hello') 84 | f.new_paragraph() 85 | f1 = formatter() 86 | f1.new_paragraph() 87 | f1.append('world') 88 | f.extend(f1) 89 | 90 | @equal('Lorem ipsum dolor sit amet, consectetur adipiscing\n' 91 | 'elit. Etiam finibus erat mi, eget accumsan metus.') 92 | def wrap(self, f): 93 | f.append(lorem) 94 | 95 | @equal(' hello\n world') 96 | def indent(self, f): 97 | with f.indent(): 98 | f.append('hello') 99 | f.append('world') 100 | 101 | @equal(' hello\nworld') 102 | def indent_individual(self, f): 103 | with f.indent(): 104 | f.append('hello') 105 | f.append('world', indent=-2) 106 | 107 | @equal(' hello\n world') 108 | def indent_extend(self, f): 109 | with f.indent(): 110 | f.append('hello') 111 | f1 = formatter() 112 | with f1.indent(): 113 | f1.append('world') 114 | f.extend(f1) 115 | 116 | @equal(' hello\n world') 117 | def indent_custom(self, f): 118 | with f.indent(3): 119 | f.append('hello') 120 | f.append('world') 121 | 122 | @equal(' Lorem ipsum dolor sit amet, consectetur\n' 123 | ' adipiscing elit. Etiam finibus erat mi, eget\n' 124 | ' accumsan metus.') 125 | def indent_wrap(self, f): 126 | with f.indent(): 127 | f.append(lorem) 128 | 129 | @equal('col1 col2\ncolumn1 column2') 130 | def columns(self, f): 131 | with f.columns() as cols: 132 | cols.append('col1', 'col2') 133 | cols.append('column1', 'column2') 134 | 135 | @equal(' col1 | col2 | col3\ncolumn1 | column2 | column3') 136 | def columns_opts(self, f): 137 | with f.columns(3, spacing=' | ', align='><>') as cols: 138 | cols.append('col1', 'col2', 'col3') 139 | cols.append('column1', 'column2', 'column3') 140 | 141 | @equal('col1 col2\n\nheading\ncolumn1 column2') 142 | def columns_iterleaved(self, f): 143 | with f.columns() as cols: 144 | cols.append('col1', 'col2') 145 | f.new_paragraph() 146 | f.append('heading') 147 | cols.append('column1', 'column2') 148 | 149 | 150 | @equal('col1 Lorem ipsum dolor sit amet, consectetur\n' 151 | ' adipiscing elit. Etiam finibus erat mi,\n' 152 | ' eget accumsan metus.') 153 | def columns_wrap_second(self, f): 154 | with f.columns() as cols: 155 | cols.append('col1', lorem) 156 | self.assertEqual([4, 43], cols.widths) 157 | 158 | @equal('Lorem ipsum Lorem ipsum dolor sit amet,\n' 159 | 'dolor sit consectetur adipiscing elit. Etiam\n' 160 | 'amet, finibus erat mi, eget accumsan\n' 161 | 'consectetur metus.\n' 162 | 'adipiscing\n' 163 | 'elit. Etiam\n' 164 | 'finibus\n' 165 | 'erat mi,\n' 166 | 'eget\n' 167 | 'accumsan\n' 168 | 'metus.') 169 | def columns_wrap_both(self, f): 170 | with f.columns(wrap=[True, True]) as cols: 171 | cols.append(lorem, lorem) 172 | self.assertEqual(cols.widths, [11, 36]) 173 | 174 | @equal('Lorem ipsum dolor sit amet\n' 175 | ' Lorem ipsum dolor sit amet, consectetur\n' 176 | ' adipiscing elit. Etiam finibus erat mi, eget\n' 177 | ' accumsan metus.') 178 | def columns_nowrap_first(self, f): 179 | with f.columns() as cols: 180 | cols.append(slorem, lorem) 181 | self.assertEqual([2, 45], cols.widths) 182 | 183 | def test_match_lines_no_empty_ends(self): 184 | f = formatter(max_width=50) 185 | cols = f.columns() 186 | cols.widths = [4, 4] 187 | cols.wrap = [True, False] 188 | self.assertEqual( 189 | list(cols.match_lines(['col1', 'word word word'])), 190 | [['col1', 'word word word']] 191 | ) 192 | 193 | @equal('row1col1 is long\n' 194 | ' word word word\n' 195 | 'col1 word word word word') 196 | def columns_nowrap_multiline(self, f): 197 | with f.columns() as cols: 198 | cols.append('row1col1 is long', 'word word word') 199 | cols.append('col1', 'word word word word') 200 | 201 | 202 | @equal(' lll1 c c c c c c c c c c c c c c c c c\n' 203 | ' c c c c c\n' 204 | ' lll2 c c c c c c c c c c c c c c c c c\n' 205 | ' c c c c c' 206 | ) 207 | def columns_wrap_indented(self, f): 208 | with f.indent(10): 209 | with f.columns() as cols: 210 | cols.append('lll1', 211 | 'c c c c c c c c c c c c c c c c c c c c c c') 212 | with f.indent(10): 213 | cols.append('lll2', 214 | 'c c c c c c c c c c c c c c c c c c c c c c') 215 | 216 | 217 | class NameConversionTests(Fixtures): 218 | def _test(self, name, exp_converted_name): 219 | self.assertEqual(exp_converted_name, util.name_py2cli(name)) 220 | 221 | one_word = "name", "name" 222 | snake_case = "compound_name", "compound-name" 223 | camel_case = "CompoundName", "compound-name" 224 | dromedary_case = "compoundName", "compound-name" 225 | camel_snake_case = "Compound_Name", "compound-name" 226 | avoiding_name = "list_", "list" 227 | private_name = "_name", "name" 228 | 229 | 230 | class KeywordNameConversionTests(Fixtures): 231 | def _test(self, name, exp_converted_name): 232 | self.assertEqual(exp_converted_name, util.name_py2cli(name, kw=True)) 233 | 234 | one_letter = "n", "-n" 235 | one_word = "name", "--name" 236 | snake_case = "compound_name", "--compound-name" 237 | camel_case = "CompoundName", "--compound-name" 238 | dromedary_case = "compoundName", "--compound-name" 239 | camel_snake_case = "Compound_Name", "--compound-name" 240 | avoiding_name = "list_", "--list" 241 | private_name = "_name", "--name" 242 | private_one_letter = "_n", "-n" 243 | -------------------------------------------------------------------------------- /clize/tests/util.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | 5 | import __future__ 6 | import os 7 | import sys 8 | import inspect 9 | import unittest 10 | from contextlib import contextmanager 11 | from io import StringIO 12 | 13 | import repeated_test 14 | from sigtools import support 15 | 16 | from clize import runner 17 | 18 | 19 | class Tests(unittest.TestCase): 20 | maxDiff = 5000 21 | 22 | def read_arguments(self, sig, args): 23 | return sig.read_arguments(args, 'test') 24 | 25 | def crun(self, func, args, stdin=None, **kwargs): 26 | orig = sys.stdin, sys.stdout, sys.stderr 27 | if stdin is None: 28 | stdin = StringIO() 29 | sys.stdin = stdin 30 | sys.stdout = stdout = StringIO() 31 | sys.stderr = stderr = StringIO() 32 | try: 33 | runner.run(func, args=args, exit=False, out=stdout, err=stderr, **kwargs) 34 | return stdout, stderr 35 | finally: 36 | sys.stdin, sys.stdout, sys.stderr = orig 37 | 38 | @contextmanager 39 | def cd(self, directory): 40 | cwd = os.getcwd() 41 | try: 42 | os.chdir(directory) 43 | yield 44 | finally: 45 | os.chdir(cwd) 46 | 47 | def assertLinesEqual(self, expected, actual): 48 | exp_split = list(filter(None, 49 | (line.rstrip() for line in inspect.cleandoc(expected).split('\n')))) 50 | act_split = list(filter(None, 51 | (line.rstrip() for line in actual.split('\n')))) 52 | self.assertEqual(exp_split, act_split) 53 | 54 | @contextmanager 55 | def maybe_expect_warning(self, maybe_warning): 56 | if maybe_warning is not None: 57 | with self.assertWarns(maybe_warning) as warn_context: 58 | yield warn_context 59 | else: 60 | yield None 61 | 62 | 63 | 64 | 65 | support_s_without_annotations_feature = repeated_test.NamedAlternative("no __future__ features", support.s) 66 | @repeated_test.NamedAlternative("with __future__.annotations") 67 | def support_s_with_annotations_feature(*args, future_features=(), **kwargs): 68 | return support.s(*args, future_features=future_features + ("annotations",), **kwargs) 69 | 70 | 71 | support_f_without_annotations_feature = repeated_test.NamedAlternative("no __future__ features", support.f) 72 | @repeated_test.NamedAlternative("with __future__.annotations") 73 | def support_f_with_annotations_feature(*args, future_features=(), **kwargs): 74 | return support.f(*args, future_features=future_features + ("annotations",), **kwargs) 75 | 76 | 77 | has_future_annotations = ( 78 | hasattr(__future__, "annotations") 79 | ) 80 | 81 | 82 | Fixtures = repeated_test.WithTestClass(Tests) 83 | tup = repeated_test.tup 84 | 85 | @repeated_test.with_options_matrix( 86 | make_signature = 87 | [support_s_without_annotations_feature, support_s_with_annotations_feature] 88 | if has_future_annotations else 89 | [support_s_without_annotations_feature] 90 | ) 91 | class SignatureFixtures(Fixtures): 92 | _test = None 93 | 94 | 95 | @repeated_test.with_options_matrix( 96 | make_function = 97 | [support_f_without_annotations_feature, support_f_with_annotations_feature] 98 | if has_future_annotations else 99 | [support_f_without_annotations_feature] 100 | ) 101 | class FunctionFixtures(Fixtures): 102 | _test = None 103 | 104 | 105 | class Matching(object): 106 | def __init__(self, condition): 107 | self.condition = condition 108 | 109 | def __eq__(self, other): 110 | return self.condition(other) 111 | 112 | 113 | @Matching 114 | def any(other): 115 | return True 116 | 117 | 118 | def any_instance_of(cls): 119 | @Matching 120 | def condition(obj): 121 | return isinstance(obj, cls) 122 | return condition 123 | -------------------------------------------------------------------------------- /clize/util.py: -------------------------------------------------------------------------------- 1 | # clize -- A command-line argument parser for Python 2 | # Copyright (C) 2011-2022 by Yann Kaiser and contributors. See AUTHORS and 3 | # COPYING for details. 4 | 5 | """various""" 6 | 7 | import os 8 | from functools import partial, update_wrapper 9 | import itertools 10 | import textwrap 11 | from difflib import SequenceMatcher 12 | from collections import OrderedDict 13 | 14 | 15 | class Sentinel(object): 16 | __slots__ = ('name') 17 | 18 | def __init__(self, name): 19 | self.name = name 20 | 21 | def __repr__(self): 22 | return self.name 23 | 24 | UNSET = Sentinel('') 25 | 26 | 27 | zip_longest = itertools.zip_longest 28 | 29 | 30 | def compute_similarity(word1, word2): 31 | seq_matcher = SequenceMatcher(None, word1, word2) 32 | return seq_matcher.ratio() 33 | 34 | 35 | def closest_option(search, options, threshold=0.6): 36 | if len(options) > 0: 37 | checker = partial(compute_similarity, search) 38 | closest_match = max(options, key=checker) 39 | if checker(closest_match) >= threshold: 40 | return closest_match 41 | return None 42 | 43 | 44 | def to_kebap_case(s): 45 | had_letter = False 46 | for c in s: 47 | if c == '_': 48 | if had_letter: 49 | had_letter = False 50 | yield '-' 51 | elif c.isupper(): 52 | if had_letter: 53 | yield '-' 54 | yield c.lower() 55 | had_letter = True 56 | else: 57 | yield c 58 | had_letter = True 59 | 60 | def name_py2cli(name, kw=False, fixcase=True): 61 | name = ''.join(to_kebap_case(name) if fixcase else name).rstrip('-') 62 | if kw: 63 | if len(name) > 1: 64 | return '--' + name 65 | else: 66 | return '-' + name 67 | else: 68 | return name 69 | 70 | def name_cli2py(name, kw=False): 71 | return name.strip('-').replace('-', '_') 72 | 73 | def name_type2cli(typ): 74 | try: 75 | convinfo = typ._clize__value_converter 76 | except AttributeError: 77 | return typ.__name__.strip('_').upper() 78 | else: 79 | return convinfo['name'] 80 | 81 | def maybe_iter(x): 82 | try: 83 | tup = tuple(x) 84 | except TypeError: 85 | return x, 86 | else: 87 | if isinstance(x, str): 88 | return x, 89 | return tup 90 | 91 | def dict_from_names(obj, receiver=None): 92 | try: 93 | obj.items 94 | except AttributeError: 95 | pass 96 | else: 97 | if receiver is None: 98 | return obj 99 | else: 100 | receiver.update(obj) 101 | return receiver 102 | if receiver is None: 103 | receiver = OrderedDict() 104 | receiver.update((x.__name__, x) for x in maybe_iter(obj)) 105 | return receiver 106 | 107 | class property_once(object): 108 | def __init__(self, func): 109 | update_wrapper(self, func) 110 | self.func = func 111 | self.key = func.__name__ 112 | 113 | def __get__(self, obj, owner): 114 | if obj is None: 115 | return self 116 | try: 117 | return obj.__dict__[self.key] # could happen if we've been 118 | # assigned to multiple names 119 | except KeyError: 120 | pass 121 | ret = obj.__dict__[self.key] = self.func(obj) 122 | return ret 123 | 124 | def __repr__(self): 125 | return ''.format(self.func) 126 | 127 | 128 | def bound(min, val, max): 129 | if min is not None and val < min: 130 | return min 131 | elif max is not None and val > max: 132 | return max 133 | else: 134 | return val 135 | 136 | 137 | class _FormatterRow(object): 138 | def __init__(self, columns, cells): 139 | self.columns = columns 140 | self.cells = cells 141 | 142 | def __iter__(self): 143 | return iter(self.cells) 144 | 145 | def __repr__(self): 146 | return "{0}({1.columns!r}, {1.cells!r})".format( 147 | type(self).__name__, self) 148 | 149 | def formatter_lines(self): 150 | return self.columns.format_cells(self.cells) 151 | 152 | 153 | def process_widths(widths, max_width): 154 | for w in widths: 155 | if isinstance(w, float): 156 | yield int(w * max_width) 157 | else: 158 | yield w 159 | 160 | 161 | class _FormatterColumns(object): 162 | def __init__(self, formatter, num, spacing, align, 163 | wrap, min_widths, max_widths, indent): 164 | self.formatter = formatter 165 | self.indent = indent 166 | self.num = num 167 | self.spacing = spacing 168 | self.align = align or '<' * num 169 | self.wrap = wrap or (False,) + (True,) * (num - 1) 170 | self.min_widths = min_widths or (2,) * num 171 | self.max_widths = max_widths or (.25,) + (None,) * (num - 1) 172 | self.rows = [] 173 | self.finished = False 174 | 175 | def __enter__(self): 176 | return self 177 | 178 | def append(self, *cells): 179 | if len(cells) != self.num: 180 | raise ValueError('expected {0} cells but got {1}'.format( 181 | self.num, len(cells))) 182 | row = _FormatterRow(self, cells) 183 | self.rows.append(row) 184 | self.formatter.append_raw(row, -self.formatter._indent) 185 | 186 | def __exit__(self, exc_type, exc_val, exc_tb): 187 | self.finished = True 188 | self.widths = list(self.compute_widths()) 189 | 190 | def compute_widths(self): 191 | used = len(self.spacing) * (self.num - 1) + self.indent 192 | space_left = self.formatter.max_width - used 193 | min_widths = list(process_widths(self.min_widths, space_left)) 194 | max_widths = list(process_widths(self.max_widths, space_left)) 195 | maxlens = [sorted(len(s) for s in col) for col in zip(*self.rows)] 196 | for i, maxlen in enumerate(maxlens): 197 | space_left = ( 198 | self.formatter.max_width 199 | - used - sum(min_widths[i+1:])) 200 | max_width = bound(None, space_left, max_widths[i]) 201 | if not self.wrap[i]: 202 | while maxlen[-1] > max_width: 203 | maxlen.pop() 204 | if not maxlen: 205 | maxlen.append(min_widths[i]) 206 | break 207 | width = bound(min_widths[i], maxlen[-1], max_width) 208 | used += width 209 | yield width 210 | 211 | 212 | def format_cells(self, cells): 213 | wcells = (self.format_cell(*args) for args in enumerate(cells)) 214 | indent = ' ' * self.indent 215 | return (indent + self.spacing.join(cline).rstrip() 216 | for cells in zip_longest(*wcells) 217 | for cline in self.match_lines(cells) 218 | ) 219 | 220 | def format_cell(self, i, cell): 221 | if self.wrap[i] or len(cell) <= self.widths[i]: 222 | width = self.widths[i] 223 | else: 224 | width = sum(self.widths[i:]) + len(self.spacing) * (self.num-i-1) 225 | for line in textwrap.wrap(cell, width): 226 | yield '{0:{1}{2}}'.format(line, self.align[i], width) 227 | 228 | def match_lines(self, cells): 229 | ret = [] 230 | for i, cell in enumerate(cells): 231 | if cell is None: 232 | cell = ' ' * self.widths[i] 233 | ret.append(cell) 234 | if len(cell) > self.widths[i]: 235 | yield ret 236 | if i + 1 == self.num: 237 | return 238 | ret = [' ' * (sum(self.widths[:i+1]) + len(self.spacing) * i)] 239 | yield ret 240 | 241 | 242 | class _FormatterIndent(object): 243 | def __init__(self, formatter, indent): 244 | self.formatter = formatter 245 | self.indent = indent 246 | 247 | def __enter__(self): 248 | self.formatter._indent += self.indent 249 | return self 250 | 251 | def __exit__(self, exc_type, exc_val, exc_tb): 252 | self.formatter._indent -= self.indent 253 | 254 | 255 | def get_terminal_width(): 256 | size = 0 257 | try: 258 | size = os.get_terminal_size().columns 259 | except (AttributeError, OSError): 260 | pass 261 | return size if size > 20 else 78 262 | 263 | 264 | class Formatter(object): 265 | delimiter = '\n' 266 | 267 | def __init__(self, max_width=None): 268 | self.max_width = ( 269 | get_terminal_width() if max_width is None else max_width) 270 | self.wrapper = textwrap.TextWrapper() 271 | self.lines = [] 272 | self._indent = 0 273 | 274 | def append(self, line, indent=0): 275 | if not line: 276 | self.new_paragraph() 277 | elif line.startswith(' '): 278 | self.append_raw(line, indent) 279 | else: 280 | self.wrapper.width = self.get_width(indent) 281 | for wline in self.wrapper.wrap(line): 282 | self.append_raw(wline, indent=indent) 283 | 284 | def append_raw(self, line, indent=0): 285 | self.lines.append((self._indent + indent, line)) 286 | 287 | def get_width(self, indent=0): 288 | return self.max_width - self._indent - indent 289 | 290 | def new_paragraph(self): 291 | if self.lines and self.lines[-1][1]: 292 | self.lines.append((0, '')) 293 | 294 | def extend(self, iterable): 295 | if not isinstance(iterable, Formatter): 296 | for line in iterable: 297 | self.append(line) 298 | else: 299 | for indent, line in iterable: 300 | self.append_raw(line, indent) 301 | 302 | def indent(self, indent=2): 303 | return _FormatterIndent(self, indent) 304 | 305 | def columns(self, num=2, spacing=' ', align=None, 306 | wrap=None, min_widths=None, max_widths=None, 307 | indent=None): 308 | return _FormatterColumns( 309 | self, num, spacing, align, 310 | wrap, min_widths, max_widths, 311 | self._indent if indent is None else indent) 312 | 313 | def __str__(self): 314 | if self.lines and not self.lines[-1][1]: 315 | lines = self.lines[:-1] 316 | else: 317 | lines = self.lines 318 | return self.delimiter.join( 319 | ' ' * indent + line 320 | for indent, line_ in lines 321 | for line in self.convert_line(line_) 322 | ) 323 | 324 | def convert_line(self, line): 325 | try: 326 | lines_getter = line.formatter_lines 327 | except AttributeError: 328 | yield str(line) 329 | else: 330 | for line in lines_getter(): 331 | yield str(line) 332 | 333 | def __iter__(self): 334 | return iter(self.lines) 335 | 336 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/clize.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/clize.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/clize" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/clize" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/_static/clize_style.css: -------------------------------------------------------------------------------- 1 | .wy-table-responsive table td { 2 | white-space: initial; 3 | min-width: 15em; 4 | } 5 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% set css_files = css_files + ['_static/clize_style.css'] %} 4 | 5 | {%- block extrahead %} 6 | 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /docs/alternatives.rst: -------------------------------------------------------------------------------- 1 | .. _clize alternatives: 2 | 3 | Alternatives to Clize 4 | ===================== 5 | 6 | Many argument parsers exist in Python. This document shortly presents the major 7 | argument parsers in the Python ecosystem and relates them to Clize. It also 8 | lists other parsers including some similar to Clize. 9 | 10 | .. note:: 11 | 12 | The code examples below are excerpts from the other parsers' respective 13 | documentation. Please see the respective links for the relevant copyright 14 | information. 15 | 16 | 17 | .. _argparse comparison: 18 | 19 | argparse 20 | -------- 21 | 22 | `argparse` is Python's standard library module for building argument parsers. 23 | It was built to replace `getopt` and `optparse`, offering more flexibility and 24 | handling of positional arguments. 25 | 26 | Here's an example from the standard library:: 27 | 28 | import argparse 29 | 30 | parser = argparse.ArgumentParser(description='Process some integers.') 31 | parser.add_argument('integers', metavar='N', type=int, nargs='+', 32 | help='an integer for the accumulator') 33 | parser.add_argument('--sum', dest='accumulate', action='store_const', 34 | const=sum, default=max, 35 | help='sum the integers (default: find the max)') 36 | 37 | args = parser.parse_args() 38 | print(args.accumulate(args.integers)) 39 | 40 | It demonstrates the use of accumulator arguments (building a list), supplying 41 | values using flags. 42 | 43 | A developer wishing to use `argparse` for their argument parsing would: 44 | 45 | 1. Instantiate a parser as above 46 | 2. Tell it what to expect using the `~argparse.ArgumentParser.add_argument` 47 | method 48 | 3. Tell it to parse the arguments 49 | 4. Execute your program logic 50 | 51 | The above example can be performed using Clize as follows:: 52 | 53 | import clize 54 | 55 | def accumulator(*integers, sum_=False): 56 | """Process some integers. 57 | 58 | :param integers: integers for the accumulator 59 | :param sum_: sum the integers (default: find the max) 60 | """ 61 | f = sum if sum_ else max 62 | return f(integers) 63 | 64 | clize.run(main) 65 | 66 | `argparse` idioms and their Clize counterpart: 67 | 68 | .. |ra| replace:: `~argparse.ArgumentParser.add_argument` 69 | .. |pna| replace:: `~argparse.ArgumentParser.parse_known_args` 70 | .. |lo| replace:: `~clize.Parameter.LAST_OPTION` 71 | 72 | +--------------------------------------+--------------------------------------+ 73 | | `argparse` | Clize | 74 | +======================================+======================================+ 75 | | API user creates a parser object and | API user creates a function. Clize | 76 | | configures parameters. | creates a CLI from it. | 77 | +--------------------------------------+--------------------------------------+ 78 | | Document the CLI using the | Document the CLI by writing a | 79 | | ``description`` and ``help`` | docstring for your function(s). | 80 | | arguments of the parser object. | | 81 | +--------------------------------------+--------------------------------------+ 82 | | `argparse` reads arguments and | Clize reads arguments and calls the | 83 | | produces an object with the state | associated function with the | 84 | | these arguments represent. | arguments translated into Python | 85 | | | equivalents. | 86 | +--------------------------------------+--------------------------------------+ 87 | | Use subparsers to create subcommands | Pass :ref:`multiple | 88 | | and share parameters across | functions` to run | 89 | | functionalities. | in order to create subcommands. | 90 | | +--------------------------------------+ 91 | | | Use :ref:`decorators ` to share parameters | 93 | | | between functions. | 94 | +--------------------------------------+--------------------------------------+ 95 | | Extend the parser using | Extend the parser using :ref:`custom | 96 | | `~argparse.Action`. | parameter classes ` and :ref:`converters | 98 | | | `. | 99 | +--------------------------------------+--------------------------------------+ 100 | | Specify converter functions for | Specify a :ref:`value converter | 101 | | arguments using the ``type`` | `. | 102 | | argument of |ra|. | | 103 | +--------------------------------------+ | 104 | | Specify the value label in | | 105 | | documentation using the ``metavar`` | | 106 | | argument. | | 107 | +--------------------------------------+--------------------------------------+ 108 | | Ask the parser to only parse known | Forward extra arguments to another | 109 | | arguments using |pna|. | function using ``*args, **kwargs``. | 110 | | +--------------------------------------+ 111 | | | Specify a parameter as | 112 | | | |lo| and | 113 | | | collect the rest in ``*args``. | 114 | +--------------------------------------+--------------------------------------+ 115 | | Specify allowed values with the | Use `~clize.parameters.one_of`. | 116 | | ``choices`` argument. | | 117 | +--------------------------------------+--------------------------------------+ 118 | | Specify quantifiers using nargs. | Use default arguments and/or use | 119 | | | `clize.parameters.multi`. | 120 | +--------------------------------------+--------------------------------------+ 121 | 122 | 123 | .. _click comparison: 124 | 125 | Click 126 | ----- 127 | 128 | `click `_ is a third-party command-line argument 129 | parsing library based on `optparse`. It aims to cater to large scale projects 130 | and was created to support `Flask `_ and its 131 | ecosystem. It also contains various utilities for working with terminal 132 | environments. 133 | 134 | :: 135 | 136 | import click 137 | 138 | @click.command() 139 | @click.option('--count', default=1, help='Number of greetings.') 140 | @click.option('--name', prompt='Your name', 141 | help='The person to greet.') 142 | def hello(count, name): 143 | """Simple program that greets NAME for a total of COUNT times.""" 144 | for x in range(count): 145 | click.echo('Hello %s!' % name) 146 | 147 | if __name__ == '__main__': 148 | hello() 149 | 150 | A `click`_ user writes a function containing some behavior. Each parameter is 151 | matched with an ``option`` or ``argument`` decorator, and this is decorated 152 | with ``command``. This function becomes a callable that will parse the 153 | arguments given to the program. 154 | 155 | It also supports nestable subcommands:: 156 | 157 | @click.group() 158 | @click.option('--debug/--no-debug', default=False) 159 | def cli(debug): 160 | click.echo('Debug mode is %s' % ('on' if debug else 'off')) 161 | 162 | @cli.command() 163 | def sync(): 164 | click.echo('Synching') 165 | 166 | `click`_ idioms and their Clize counterpart: 167 | 168 | +--------------------------------------+--------------------------------------+ 169 | | `click`_ | Clize | 170 | +======================================+======================================+ 171 | | API user creates a function and | API user creates a function. Clize | 172 | | configures parameters using | creates a CLI from it. API user can | 173 | | decorators. | specify options using parameter | 174 | | | annotations. | 175 | +--------------------------------------+--------------------------------------+ 176 | | Subcommands are created by using the | Subcommands are created by passing a | 177 | | ``group`` decorator then the | dict or iterable to `clize.run`. It | 178 | | ``command`` method. | is possible to extend Clize to do it | 179 | | | like click. | 180 | +--------------------------------------+--------------------------------------+ 181 | | Command group functions can parse | :ref:`Decorators ` can be used to share | 183 | | | parameters between functions. | 184 | +--------------------------------------+--------------------------------------+ 185 | | Use ``pass_context`` to share global | Use `~.parameters.value_inserter` | 186 | | state between functions. | and the | 187 | | | `~.parser.CliBoundArguments.meta` | 188 | | | dict to share global state between | 189 | | | functions without using parameters. | 190 | +--------------------------------------+--------------------------------------+ 191 | | Add conversion types by extending | Add conversion types with the | 192 | | ``ParamType``. | `~.parser.value_converter` | 193 | | | decorator. | 194 | +--------------------------------------+--------------------------------------+ 195 | 196 | 197 | .. _docopt comparison: 198 | 199 | Docopt 200 | ------ 201 | 202 | `docopt `_ is a command-line interface description language 203 | with parsers implemented in several languages. 204 | 205 | :: 206 | 207 | """Naval Fate. 208 | 209 | Usage: 210 | naval_fate.py ship new ... 211 | naval_fate.py ship move [--speed=] 212 | naval_fate.py ship shoot 213 | naval_fate.py mine (set|remove) [--moored | --drifting] 214 | naval_fate.py (-h | --help) 215 | naval_fate.py --version 216 | 217 | Options: 218 | -h --help Show this screen. 219 | --version Show version. 220 | --speed= Speed in knots [default: 10]. 221 | --moored Moored (anchored) mine. 222 | --drifting Drifting mine. 223 | 224 | """ 225 | from docopt import docopt 226 | 227 | 228 | if __name__ == '__main__': 229 | arguments = docopt(__doc__, version='Naval Fate 2.0') 230 | print(arguments) 231 | 232 | A `docopt`_ user will write a string containing the help page for the command 233 | (as would be displayed when using ``--help``) and hand it to `docopt`_. It will 234 | parse arguments from the command-line and produce a `dict`-like object with the 235 | values provided. The user then has to dispatch to the relevant code depending 236 | on this object. 237 | 238 | +--------------------------------------+--------------------------------------+ 239 | | `docopt`_ | Clize | 240 | +======================================+======================================+ 241 | | API user writes a formatted help | API user writes Python functions and | 242 | | string which docopt parses and draws | Clize draws a CLI from them. | 243 | | a CLI from. | | 244 | +--------------------------------------+--------------------------------------+ 245 | | `docopt`_ parses arguments and | Clize parses arguments and calls | 246 | | returns a `dict`-like object mapping | your function, with the arguments | 247 | | parameters to strings. | converted to Python types. | 248 | +--------------------------------------+--------------------------------------+ 249 | | The string passed to `docopt`_ is | Clize creates the help output from | 250 | | used for help output directly. This | the function signature and fetches | 251 | | help output does not reflow | parameter descriptions from the | 252 | | depending on terminal size. | docstring. The user can reorder | 253 | | | option descriptions, label them and | 254 | | | add paragraphs. The output is | 255 | | | adapted to the output terminal | 256 | | | width. | 257 | +--------------------------------------+--------------------------------------+ 258 | | The usage line is printed on parsing | A relevant message and/or suggestion | 259 | | errors. | is displayed on error. | 260 | +--------------------------------------+--------------------------------------+ 261 | | Specify exclusivity constraints in | Use Python code inside your function | 262 | | the usage signature. | (or decorator) or custom parameters | 263 | | | to specify exclusivity constraints. | 264 | +--------------------------------------+--------------------------------------+ 265 | | The entire CLI must be defined in | You can compose your CLI using | 266 | | one string. | subcommands, function decorators, | 267 | | | function composition, parameter | 268 | | | decorators, ... | 269 | +--------------------------------------+--------------------------------------+ 270 | 271 | 272 | .. _similar comparisons: 273 | 274 | Other parsers similar to Clize 275 | ------------------------------ 276 | 277 | Parsers based on `argparse` 278 | ........................... 279 | 280 | 281 | .. _defopt comparison: 282 | 283 | `defopt `_ is similar to Clize: it uses 284 | annotations to supplement the default configurations for parameters. A notable 285 | difference is that it supports Sphinx-compatible docstrings, but does not 286 | support composition. 287 | 288 | .. _argh comparison: 289 | 290 | With `argh `_ you can amend these 291 | parameter definitions (or add new parameters) using a decorator that takes the 292 | same arguments as `argparse.ArgumentParser.add_argument`. 293 | 294 | .. _fire comparison: 295 | 296 | `fire `_ also converts callables to 297 | CLIs. It observes slightly different conventions than common CLIs and doesn't 298 | support keyword-only parameters. Instead, all parameters can be passed by 299 | position or by name. It does not help you generate help, though ``./program -- 300 | --help`` will print the docstring, usage information, and other technical 301 | information. It allows chaining commands with each taking the output of the 302 | previoous command. 303 | 304 | .. _other similar argparse: 305 | 306 | And then some more: 307 | 308 | * `plac `_ 309 | * `aaargh `_ -- Deprecated in favor of `click`_ 310 | 311 | 312 | .. _other similar: 313 | 314 | Other similar parsers 315 | ..................... 316 | 317 | * `CLIArgs `_ 318 | * `baker `_ -- Discontinued 319 | 320 | 321 | Other parsers 322 | ------------- 323 | 324 | * `Clint `_ -- Multiple CLI tools, 325 | including a schemaless argument parser 326 | * `twisted.usage 327 | `_ -- 328 | subclass-based approach 329 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | Running functions 5 | ----------------- 6 | 7 | .. module:: clize.runner 8 | 9 | .. autofunction:: clize.run 10 | 11 | 12 | .. autoclass:: clize.Clize 13 | 14 | .. autoclass:: clize.SubcommandDispatcher 15 | 16 | Parser 17 | ------ 18 | 19 | .. module:: clize.parser 20 | 21 | .. autoclass:: CliSignature 22 | :exclude-members: converter 23 | 24 | .. autofunction:: parameter_converter 25 | 26 | .. autofunction:: default_converter 27 | 28 | .. autofunction:: use_class 29 | 30 | .. autofunction:: use_mixin 31 | 32 | .. autoclass:: CliBoundArguments 33 | :no-undoc-members: 34 | 35 | .. autoclass:: clize.Parameter 36 | :show-inheritance: 37 | :exclude-members: L, I, U, R 38 | 39 | .. autoclass:: clize.parser.ParameterWithSourceEquivalent 40 | :show-inheritance: 41 | 42 | .. autoclass:: clize.parser.HelperParameter 43 | :show-inheritance: 44 | 45 | .. autoclass:: clize.parser.ParameterWithValue 46 | :show-inheritance: 47 | 48 | .. autofunction:: value_converter 49 | 50 | .. autoclass:: clize.parser.NamedParameter 51 | :show-inheritance: 52 | 53 | .. autoclass:: clize.parser.FlagParameter 54 | :show-inheritance: 55 | 56 | .. autoclass:: clize.parser.OptionParameter 57 | :show-inheritance: 58 | 59 | .. autoclass:: clize.parser.IntOptionParameter 60 | :show-inheritance: 61 | 62 | .. autoclass:: clize.parser.PositionalParameter 63 | :show-inheritance: 64 | 65 | .. autoclass:: clize.parser.MultiParameter 66 | :show-inheritance: 67 | 68 | .. autoclass:: clize.parser.ExtraPosArgsParameter 69 | :show-inheritance: 70 | 71 | .. autoclass:: clize.parser.AppendArguments 72 | :show-inheritance: 73 | 74 | .. autoclass:: clize.parser.IgnoreAllArguments 75 | :show-inheritance: 76 | 77 | .. autoclass:: clize.parser.FallbackCommandParameter 78 | :show-inheritance: 79 | 80 | .. autoclass:: clize.parser.AlternateCommandParameter 81 | :show-inheritance: 82 | 83 | 84 | Exceptions 85 | ---------- 86 | 87 | .. currentmodule:: None 88 | 89 | .. class:: clize.UserError(message) 90 | clize.errors.UserError(message) 91 | 92 | An error to be displayed to the user. 93 | 94 | If `clize.run` catches this error, the error will be printed without the 95 | associated traceback. 96 | 97 | .. code-block:: python 98 | 99 | def main(): 100 | raise clize.UserError("an error message") 101 | 102 | clize.run(main) 103 | 104 | .. code-block:: shell 105 | 106 | $ python usererror_example.py 107 | usererror_example.py: an error message 108 | 109 | You can also specify other exception classes to be caught using 110 | `clize.run`'s ``catch`` argument. However exceptions not based on 111 | `~clize.UserError` will not have the command name displayed. 112 | 113 | .. class:: clize.ArgumentError(message) 114 | clize.errors.ArgumentError(message) 115 | 116 | An error related to argument parsing. If `clize.run` catches this error, 117 | the command's usage line will be printed. 118 | 119 | .. code-block:: python 120 | 121 | def main(i:int): 122 | if i < 0: 123 | raise clize.ArgumentError("i must be positive") 124 | 125 | clize.run(main) 126 | 127 | .. code-block:: shell 128 | 129 | $ python argumenterror_example.py -- -5 130 | argumenterror_example.py: i must be positive 131 | Usage: argumenterror_example.py i 132 | 133 | .. automodule:: clize.errors 134 | :show-inheritance: 135 | :no-undoc-members: 136 | :exclude-members: UserError,ArgumentError 137 | 138 | 139 | Help generation 140 | --------------- 141 | 142 | .. automodule:: clize.help 143 | :show-inheritance: 144 | :members: 145 | :no-undoc-members: 146 | 147 | 148 | Compatibility with older clize releases 149 | ------------------------------------- 150 | 151 | .. module:: clize.legacy 152 | 153 | .. autofunction:: clize.clize 154 | 155 | .. autofunction:: clize.make_flag 156 | -------------------------------------------------------------------------------- /docs/basics.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: clize 2 | 3 | 4 | .. |pip| replace:: pip 5 | .. _pip: https://pip.pypa.io/en/latest/ 6 | 7 | .. |virtualenv| replace:: virtualenv 8 | .. _virtualenv: https://virtualenv.pypa.io/en/latest/ 9 | 10 | 11 | .. _basics: 12 | 13 | Basics tutorial 14 | =============== 15 | 16 | 17 | .. _install: 18 | 19 | Installation 20 | ------------ 21 | 22 | You can install clize using |pip|_. If in an activated |virtualenv|_, type: 23 | 24 | .. code-block:: console 25 | 26 | pip install clize 27 | 28 | If you wish to do a user-wide install: 29 | 30 | .. code-block:: console 31 | 32 | pip install --user clize 33 | 34 | 35 | A minimal application 36 | --------------------- 37 | 38 | A minimal command-line application written with clize consists of writing 39 | a function and passing it to :func:`run`: 40 | 41 | .. literalinclude:: /../examples/helloworld.py 42 | 43 | If you save this as *helloworld.py* and run it, the function will be run:: 44 | 45 | $ python3 helloworld.py 46 | Hello world! 47 | 48 | In this example, :func:`run` simply takes our function, runs it and prints 49 | the result. 50 | 51 | Requiring arguments 52 | ------------------- 53 | 54 | You can require arguments the same way as you would in any Python function 55 | definition. To illustrate, lets write an ``echo`` command. 56 | 57 | :: 58 | 59 | from clize import run 60 | 61 | def echo(word): 62 | return word 63 | 64 | if __name__ == '__main__': 65 | run(echo) 66 | 67 | Save it as *echo.py* and run it. You will notice the script requires exactly 68 | one argument now:: 69 | 70 | $ python3 ./echo.py 71 | ./echo.py: Missing required arguments: word 72 | Usage: ./echo.py [OPTIONS] word 73 | 74 | :: 75 | 76 | $ python3 ./echo.py ham 77 | ham 78 | 79 | :: 80 | 81 | $ python3 ./echo.py ham spam 82 | ./echo.py: Received extra arguments: spam 83 | Usage: ./echo.py [OPTIONS] word 84 | 85 | Enhancing the ``--help`` message 86 | -------------------------------- 87 | 88 | If you try to specify ``--help`` when running either of the previous examples, 89 | you will notice that Clize has in fact also generated a ``--help`` feature for 90 | you already:: 91 | 92 | $ python3 ./echo.py --help 93 | Usage: ./echo.py [OPTIONS] word 94 | 95 | Positional arguments: 96 | word 97 | 98 | Other actions: 99 | -h, --help Show the help 100 | 101 | It is fairly unhelpful right now, so we should improve that by giving our 102 | function a docstring:: 103 | 104 | def echo(word): 105 | """Echoes word back 106 | 107 | :param word: One word or quoted string to echo back 108 | """ 109 | return word 110 | 111 | As you would expect, it translates to this:: 112 | 113 | $ python3 ./echo.py --help 114 | Usage: ./echo.py [OPTIONS] word 115 | 116 | Echoes word back 117 | 118 | Positional arguments: 119 | word One word or quoted string to echo back 120 | 121 | Other actions: 122 | -h, --help Show the help 123 | 124 | .. seealso:: :ref:`docstring` 125 | 126 | 127 | .. _tutorial options: 128 | 129 | Accepting options 130 | ----------------- 131 | 132 | Clize will treat any regular parameter of your function as a positional 133 | parameter of the resulting command. To specify an option to be passed by 134 | name, you will need to use keyword-only parameters. 135 | 136 | Let's add a pair of options to specify a prefix and suffix around each line of 137 | ``word``:: 138 | 139 | def echo(word, *, prefix='', suffix=''): 140 | """Echoes text back 141 | 142 | :param word: One word or quoted string to echo back 143 | :param prefix: Prepend this to each line in word 144 | :param suffix: Append this to each line in word 145 | """ 146 | if prefix or suffix: 147 | return '\n'.join(prefix + line + suffix 148 | for line in word.split('\n')) 149 | return word 150 | 151 | In Python, any parameters after ``*args`` or ``*`` become keyword-only: When 152 | calling a function with such parameters, you can only provide a value for them 153 | by name, i.e.:: 154 | 155 | echo(word, prefix='b', suffix='a') # good 156 | echo(word, 'b', 'a') # invalid 157 | 158 | Clize then treats keyword-only parameters as options rather than as positional 159 | parameters. 160 | 161 | The change reflects on the command and its help when run:: 162 | 163 | $ python3 ./echo.py --prefix x: --suffix :y 'spam 164 | $ ham' 165 | x:spam:y 166 | x:ham:y 167 | 168 | :: 169 | 170 | $ python3 ./echo.py --help 171 | Usage: ./echo.py [OPTIONS] word 172 | 173 | Echoes text back 174 | 175 | Positional arguments: 176 | word One word or quoted string to echo back 177 | 178 | Options: 179 | --prefix=STR Prepend this to each line in word(default: ) 180 | --suffix=STR Append this to each line in word(default: ) 181 | 182 | Other actions: 183 | -h, --help Show the help 184 | 185 | .. seealso:: :ref:`name conversion` 186 | 187 | 188 | Collecting all positional arguments 189 | ----------------------------------- 190 | 191 | Just like when defining a regular Python function, you can prefix a parameter 192 | with one asterisk and it will collect all remaining positional arguments:: 193 | 194 | def echo(*text, prefix='', suffix=''): 195 | ... 196 | 197 | However, just like in Python, this makes the parameter optional. To require 198 | that it should receive at least one argument, you will have to tell Clize that 199 | ``text`` is required using an annotation:: 200 | 201 | from clize import Parameter, run 202 | 203 | def echo(*text:Parameter.REQUIRED, prefix='', suffix=''): 204 | """Echoes text back 205 | 206 | :param text: The text to echo back 207 | :param prefix: Prepend this to each line in word 208 | :param suffix: Append this to each line in word 209 | """ 210 | text = ' '.join(text) 211 | if prefix or suffix: 212 | return '\n'.join(prefix + line + suffix 213 | for line in text.split('\n')) 214 | return text 215 | 216 | if __name__ == '__main__': 217 | run(echo) 218 | 219 | 220 | Accepting flags 221 | --------------- 222 | 223 | Parameters which default to ``False`` are treated as flags. Let's add a flag 224 | to reverse the input:: 225 | 226 | def echo(*text:Parameter.REQUIRED, prefix='', suffix='', reverse=False): 227 | """Echoes text back 228 | 229 | :param text: The text to echo back 230 | :param reverse: Reverse text before processing 231 | :param prefix: Prepend this to each line in word 232 | :param suffix: Append this to each line in word 233 | 234 | """ 235 | text = ' '.join(text) 236 | if reverse: 237 | text = text[::-1] 238 | if prefix or suffix: 239 | return '\n'.join(prefix + line + suffix 240 | for line in text.split('\n')) 241 | return text 242 | 243 | :: 244 | 245 | $ python3 ./echo.py --reverse hello world 246 | dlrow olleh 247 | 248 | Converting arguments 249 | -------------------- 250 | 251 | Clize automatically tries to convert arguments to the type of the receiving 252 | parameter's default value. So if you specify an inteteger as default value, 253 | Clize will always give you an integer:: 254 | 255 | def echo(*text:Parameter.REQUIRED, 256 | prefix='', suffix='', reverse=False, repeat=1): 257 | """Echoes text back 258 | 259 | :param text: The text to echo back 260 | :param reverse: Reverse text before processing 261 | :param repeat: Amount of times to repeat text 262 | :param prefix: Prepend this to each line in word 263 | :param suffix: Append this to each line in word 264 | 265 | """ 266 | text = ' '.join(text) 267 | if reverse: 268 | text = text[::-1] 269 | text = text * repeat 270 | if prefix or suffix: 271 | return '\n'.join(prefix + line + suffix 272 | for line in text.split('\n')) 273 | return text 274 | 275 | :: 276 | 277 | $ python3 ./echo.py --repeat 3 spam 278 | spamspamspam 279 | 280 | Aliasing options 281 | ---------------- 282 | 283 | Now what we have a bunch of options, it would be helpful to have shorter names 284 | for them. You can specify aliases for them by annotating the corresponding 285 | parameter:: 286 | 287 | def echo(*text:Parameter.REQUIRED, 288 | prefix:'p'='', suffix:'s'='', reverse:'r'=False, repeat:'n'=1): 289 | ... 290 | 291 | :: 292 | 293 | $ python3 ./echo.py --help 294 | Usage: ./echo.py [OPTIONS] text... 295 | 296 | Echoes text back 297 | 298 | Positional arguments: 299 | text The text to echo back 300 | 301 | Options: 302 | -r, --reverse Reverse text before processing 303 | -n, --repeat=INT Amount of times to repeat text(default: 1) 304 | -p, --prefix=STR Prepend this to each line in word(default: ) 305 | -s, --suffix=STR Append this to each line in word(default: ) 306 | 307 | Other actions: 308 | -h, --help Show the help 309 | 310 | 311 | .. _arbitrary requirements: 312 | 313 | Arbitrary requirements 314 | ---------------------- 315 | 316 | Let's say we want to give an error if the word *spam* is in the text. To do so, 317 | one option is to raise an :class:`ArgumentError` from within your function: 318 | 319 | .. literalinclude:: /../examples/echo.py 320 | :emphasize-lines: 14-15 321 | 322 | :: 323 | 324 | $ ./echo.py spam bacon and eggs 325 | ./echo.py: I don't want any spam! 326 | Usage: ./echo.py [OPTIONS] text... 327 | 328 | If you would like the usage line not to be printed, raise :class:`.UserError` 329 | instead. 330 | 331 | 332 | Next up, we will look at how you can have Clize :ref:`dispatch to multiple 333 | functions` for you. 334 | -------------------------------------------------------------------------------- /docs/compositing.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: clize 2 | 3 | .. |decorator| replace:: 4 | `~sigtools.wrappers.decorator` 5 | 6 | .. _function compositing: 7 | .. _function-compositing: 8 | 9 | Function compositing 10 | ==================== 11 | 12 | One of Python's strengths is how easy it is to manipulate functions and combine 13 | them. However, this often breaks tools which rely on introspection to function. 14 | 15 | This isn't the case with Clize, which uses `sigtools` to understand how your 16 | functions expect to be called. 17 | 18 | Let's write some decorators and see how they integrate with Clize! 19 | 20 | Creating decorators is useful if you want to share behaviour across multiple 21 | functions passed to `run`, such as extra parameters or input/output formatting. 22 | 23 | 24 | .. _change return deco: 25 | 26 | Using a decorator to add new parameters and modify the return value 27 | ------------------------------------------------------------------- 28 | 29 | Let's create a decorator that transforms the output of the wrapped function 30 | when passed a specific flag. 31 | 32 | .. literalinclude:: /../examples/deco_add_param.py 33 | :lines: 1,3-16 34 | :emphasize-lines: 11 35 | 36 | |decorator| lets our ``with_uppercase`` function decorate other functions: 37 | 38 | .. literalinclude:: /../examples/deco_add_param.py 39 | :lines: 2-4,19-32 40 | 41 | Every time ``hello_world`` is called, ``with_uppercase`` will be called with 42 | the decorated function as first argument (``wrapped``). 43 | 44 | 45 | .. note:: 46 | 47 | `sigtools.wrappers.decorator` is used here to create decorators. It offers 48 | a simple and convenient way of creating decorators in a reliable way. 49 | 50 | However, you don't need to use it to make use of decorators with Clize and 51 | you may use other means of creating decorators if you wish. 52 | 53 | 54 | Clize will treat ``hello_world`` as if it had the same signature as:: 55 | 56 | def hello_world(name=None, *, uppercase=False): 57 | pass 58 | 59 | This is the signature you would get by "putting" the parameters of the 60 | decorated function in place of the wrapper's ``*args, **kwargs``. 61 | 62 | When you run this function, the CLI parameters will automatically match the 63 | combined signature:: 64 | 65 | $ python3 examples/decorators.py --uppercase 66 | HELLO WORLD! 67 | $ python3 examples/decorators.py john 68 | Hello john 69 | $ python3 examples/decorators.py john --uppercase 70 | HELLO JOHN 71 | 72 | The help system will also adapt and will read parameter descriptions from the 73 | decorator's docstring:: 74 | 75 | $ python decorators.py --help 76 | Usage: decorators.py [OPTIONS] [name] 77 | 78 | Says hello world 79 | 80 | Positional arguments: 81 | name Who to say hello to 82 | 83 | Formatting options: 84 | --uppercase Print output in capitals 85 | 86 | Other actions: 87 | -h, --help Show the help 88 | 89 | 90 | .. _add arg deco: 91 | 92 | Providing an argument using a decorator 93 | --------------------------------------- 94 | 95 | You can also provide the decorated function with additional arguments much in 96 | the same way. 97 | 98 | .. literalinclude:: /../examples/deco_provide_arg.py 99 | :lines: 1,3-16 100 | :emphasize-lines: 15 101 | 102 | Simply provide an additional argument to the wrapped function. It will 103 | automatically be skipped during argument parsing and will be omitted from 104 | the help. 105 | 106 | You can apply the decorator like before, with each decorated function receiving 107 | the ``branch`` argument as supplied by the decorator. 108 | 109 | .. literalinclude:: /../examples/deco_provide_arg.py 110 | :lines: 2,17-42 111 | 112 | 113 | .. _ex arg deco: 114 | 115 | Using a composed function to process arguments to a parameter 116 | ------------------------------------------------------------- 117 | 118 | You can use `clize.parameters.argument_decorator` to have a separate function 119 | process an argument while adding parameters of its own. It's like having a 120 | mini argument parser just for one argument: 121 | 122 | .. code-block:: python 123 | 124 | from clize import run 125 | from clize.parameters import argument_decorator 126 | 127 | 128 | @argument_decorator 129 | def read_server(arg, *, port=80, _6=False): 130 | """ 131 | Options for {param}: 132 | 133 | :parser port: Which port to connect on 134 | :parser _6: Use IPv6? 135 | """ 136 | return (arg, port, _6) 137 | 138 | 139 | def get_page(server:read_server, path): 140 | """ 141 | :parser server: The server to contact 142 | :parser path: The path of the resource to fetch 143 | """ 144 | print("Connecting to", server, "to get", path) 145 | 146 | 147 | run(get_page) 148 | 149 | ``read_server``'s parameters will be available on the CLI. When a value is read 150 | that would feed the ``server`` parameter, ``read_server`` is called with it and 151 | its collected arguments. Its return value is then used as the ``server`` parameter of ``get_page``: 152 | 153 | .. code-block:: console 154 | 155 | $ python argdeco.py --help 156 | Usage: argdeco.py [OPTIONS] [--port=INT] [-6] server path 157 | 158 | Arguments: 159 | server The server to contact 160 | path The path of the resource to fetch 161 | 162 | Options for server: 163 | --port=INT Which port to connect on (default: 80) 164 | -6 Use IPv6? 165 | 166 | Other actions: 167 | -h, --help Show the help 168 | 169 | A few notes: 170 | 171 | * Besides ``arg`` which receives the original value, you can only use 172 | keyword-only parameters 173 | * The decorator's docstring is used to document its parameters. It can be 174 | preferable to use a :ref:`section ` in order to distinguish 175 | them from other parameters. 176 | * Appearances of ``{param}`` in the docstring are replaced with the parameter's 177 | name. 178 | * Parameter names must not conflict with other parameters. 179 | 180 | You can also use this on named parameters, but this gets especially interesting 181 | on ``*args`` parameters, as each argument meant for it can have its own options: 182 | 183 | .. code-block:: python 184 | 185 | from clize import run 186 | from clize.parameters import argument_decorator 187 | 188 | 189 | @argument_decorator 190 | def read_server(arg, *, port=80, _6=False): 191 | """ 192 | Options for {param}: 193 | 194 | :param port: Which port to connect on 195 | :param _6: Use IPv6? 196 | """ 197 | return (arg, port, _6) 198 | 199 | 200 | def get_page(path, *servers:read_server): 201 | """ 202 | :param server: The server to contact 203 | :param path: The path of the resource to fetch 204 | """ 205 | print("Connecting to", servers, "to get", path) 206 | 207 | 208 | run(get_page) 209 | 210 | .. code-block:: console 211 | 212 | $ python argdeco.py --help 213 | Usage: argdeco.py [OPTIONS] path [[--port=INT] [-6] servers...] 214 | 215 | Arguments: 216 | path The path of the resource to fetch 217 | servers... 218 | 219 | Options for servers: 220 | --port=INT Which port to connect on (default: 80) 221 | -6 Use IPv6? 222 | 223 | Other actions: 224 | -h, --help Show the help 225 | $ python argdeco.py -6 abc 226 | argdeco.py: Missing required arguments: servers 227 | Usage: argdeco.py [OPTIONS] path [[--port=INT] [-6] servers...] 228 | $ python argdeco.py /eggs -6 abc 229 | Connecting to (('abc', 80, True),) to get /eggs 230 | $ python argdeco.py /eggs -6 abc def 231 | Connecting to (('abc', 80, True), ('def', 80, False)) to get /eggs 232 | $ python argdeco.py /eggs -6 abc def --port 8080 cheese 233 | Connecting to (('abc', 80, True), ('def', 80, False), ('cheese', 8080, False)) to get /eggs 234 | 235 | 236 | Congratulations, you've reached the end of the tutorials! You can check out the 237 | :ref:`parameter reference` or see how you can :ref:`extend 238 | the parser`. 239 | 240 | If you're stuck, need help or simply wish to give feedback you can chat using 241 | your GitHub or Twitter handle `on Gitter `_. 242 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # clize documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Sep 29 22:54:15 2013. 6 | # 7 | # This file is execfile()d with the current directory set to its containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | import re 17 | import sys 18 | 19 | import pkg_resources 20 | 21 | 22 | sys.path.insert(0, '') 23 | 24 | on_rtd = os.environ.get('READTHEDOCS', False) == 'True' 25 | 26 | # If extensions (or modules to document with autodoc) are in another directory, 27 | # add these directories to sys.path here. If the directory is relative to the 28 | # documentation root, use os.path.abspath to make it absolute, like shown here. 29 | #sys.path.insert(0, os.path.abspath('.')) 30 | 31 | # -- General configuration ----------------------------------------------------- 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | #needs_sphinx = '1.0' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be extensions 37 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 38 | extensions = [ 39 | 'sphinx.ext.autodoc', 40 | 'sphinx.ext.intersphinx', 41 | 'sphinx.ext.coverage', 42 | 'sphinx.ext.viewcode', 43 | 'sigtools.sphinxext', 44 | # 'clize._sphinx', 45 | ] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix of source filenames. 51 | source_suffix = '.rst' 52 | 53 | # The encoding of source files. 54 | #source_encoding = 'utf-8-sig' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # General information about the project. 60 | project = 'clize' 61 | copyright = '2013-2021, Yann Kaiser' 62 | 63 | 64 | 65 | for dist in pkg_resources.working_set: 66 | if dist.project_name == 'clize': 67 | release = dist.version 68 | version = re.split('[abc]', release)[0] 69 | break 70 | else: 71 | if on_rtd: 72 | raise ValueError("Clize not installed") 73 | else: 74 | release = 'UNKNOWN' 75 | version = 'UNKNOWN' 76 | 77 | # The version info for the project you're documenting, acts as replacement for 78 | # |version| and |release|, also used in various other places throughout the 79 | # built documents. 80 | # 81 | # The short X.Y version. 82 | # version = '3.0' 83 | # The full version, including alpha/beta/rc tags. 84 | # release = '3.0' 85 | 86 | # The language for content autogenerated by Sphinx. Refer to documentation 87 | # for a list of supported languages. 88 | #language = None 89 | 90 | # There are two options for replacing |today|: either, you set today to some 91 | # non-false value, then it is used: 92 | #today = '' 93 | # Else, today_fmt is used as the format for a strftime call. 94 | #today_fmt = '%B %d, %Y' 95 | 96 | # List of patterns, relative to source directory, that match files and 97 | # directories to ignore when looking for source files. 98 | exclude_patterns = ['_build'] 99 | 100 | # The reST default role (used for this markup: `text`) to use for all documents. 101 | default_role = 'py:obj' 102 | nitpicky = True 103 | nitpick_ignore_regex = [ 104 | (r'py:.*', r'^sequence.*'), 105 | (r'py:.*', r'^iterable.*'), 106 | (r'py:.*', r'^file$'), 107 | (r'py:.*', r'^function$'), 108 | (r'py:.*', r'^callable$'), 109 | ] 110 | 111 | # If true, '()' will be appended to :func: etc. cross-reference text. 112 | #add_function_parentheses = True 113 | 114 | # If true, the current module name will be prepended to all description 115 | # unit titles (such as .. function::). 116 | #add_module_names = True 117 | 118 | # If true, sectionauthor and moduleauthor directives will be shown in the 119 | # output. They are ignored by default. 120 | #show_authors = False 121 | 122 | # The name of the Pygments (syntax highlighting) style to use. 123 | pygments_style = 'sphinx' 124 | 125 | # A list of ignored prefixes for module index sorting. 126 | #modindex_common_prefix = [] 127 | 128 | 129 | # -- Options for HTML output --------------------------------------------------- 130 | 131 | # The theme to use for HTML and HTML Help pages. See the documentation for 132 | # a list of builtin themes. 133 | html_theme = 'default' if on_rtd else 'sphinx_rtd_theme' 134 | 135 | # Theme options are theme-specific and customize the look and feel of a theme 136 | # further. For a list of options available for each theme, see the 137 | # documentation. 138 | #html_theme_options = {} 139 | 140 | # Add any paths that contain custom themes here, relative to this directory. 141 | #html_theme_path = [] 142 | 143 | # The name for this set of Sphinx documents. If None, it defaults to 144 | # " v documentation". 145 | #html_title = None 146 | 147 | # A shorter title for the navigation bar. Default is the same as html_title. 148 | #html_short_title = None 149 | 150 | # The name of an image file (relative to this directory) to place at the top 151 | # of the sidebar. 152 | #html_logo = None 153 | 154 | # The name of an image file (within the static path) to use as favicon of the 155 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 156 | # pixels large. 157 | #html_favicon = None 158 | 159 | # Add any paths that contain custom static files (such as style sheets) here, 160 | # relative to this directory. They are copied after the builtin static files, 161 | # so a file named "default.css" will overwrite the builtin "default.css". 162 | html_static_path = ['_static'] 163 | 164 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 165 | # using the given strftime format. 166 | #html_last_updated_fmt = '%b %d, %Y' 167 | 168 | # If true, SmartyPants will be used to convert quotes and dashes to 169 | # typographically correct entities. 170 | #html_use_smartypants = True 171 | 172 | # Custom sidebar templates, maps document names to template names. 173 | #html_sidebars = {} 174 | 175 | # Additional templates that should be rendered to pages, maps page names to 176 | # template names. 177 | #html_additional_pages = {} 178 | 179 | # If false, no module index is generated. 180 | #html_domain_indices = True 181 | 182 | # If false, no index is generated. 183 | #html_use_index = True 184 | 185 | # If true, the index is split into individual pages for each letter. 186 | #html_split_index = False 187 | 188 | # If true, links to the reST sources are added to the pages. 189 | #html_show_sourcelink = True 190 | 191 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 192 | #html_show_sphinx = True 193 | 194 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 195 | #html_show_copyright = True 196 | 197 | # If true, an OpenSearch description file will be output, and all pages will 198 | # contain a tag referring to it. The value of this option must be the 199 | # base URL from which the finished HTML is served. 200 | #html_use_opensearch = '' 201 | 202 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 203 | #html_file_suffix = None 204 | 205 | # Output file base name for HTML help builder. 206 | htmlhelp_basename = 'clizedoc' 207 | 208 | 209 | # -- Options for LaTeX output -------------------------------------------------- 210 | 211 | latex_elements = { 212 | # The paper size ('letterpaper' or 'a4paper'). 213 | #'papersize': 'letterpaper', 214 | 215 | # The font size ('10pt', '11pt' or '12pt'). 216 | #'pointsize': '10pt', 217 | 218 | # Additional stuff for the LaTeX preamble. 219 | #'preamble': '', 220 | } 221 | 222 | # Grouping the document tree into LaTeX files. List of tuples 223 | # (source start file, target name, title, author, documentclass [howto/manual]). 224 | latex_documents = [ 225 | ('index', 'clize.tex', 'clize Documentation', 226 | 'Yann Kaiser', 'manual'), 227 | ] 228 | 229 | # The name of an image file (relative to this directory) to place at the top of 230 | # the title page. 231 | #latex_logo = None 232 | 233 | # For "manual" documents, if this is true, then toplevel headings are parts, 234 | # not chapters. 235 | #latex_use_parts = False 236 | 237 | # If true, show page references after internal links. 238 | #latex_show_pagerefs = False 239 | 240 | # If true, show URL addresses after external links. 241 | #latex_show_urls = False 242 | 243 | # Documents to append as an appendix to all manuals. 244 | #latex_appendices = [] 245 | 246 | # If false, no module index is generated. 247 | #latex_domain_indices = True 248 | 249 | 250 | # -- Options for manual page output -------------------------------------------- 251 | 252 | # One entry per manual page. List of tuples 253 | # (source start file, name, description, authors, manual section). 254 | man_pages = [ 255 | ('index', 'clize', 'clize Documentation', 256 | ['Yann Kaiser'], 1) 257 | ] 258 | 259 | # If true, show URL addresses after external links. 260 | #man_show_urls = False 261 | 262 | 263 | # -- Options for Texinfo output ------------------------------------------------ 264 | 265 | # Grouping the document tree into Texinfo files. List of tuples 266 | # (source start file, target name, title, author, 267 | # dir menu entry, description, category) 268 | texinfo_documents = [ 269 | ('index', 'clize', 'clize Documentation', 270 | 'Yann Kaiser', 'clize', 'One line description of project.', 271 | 'Miscellaneous'), 272 | ] 273 | 274 | # Documents to append as an appendix to all manuals. 275 | #texinfo_appendices = [] 276 | 277 | # If false, no module index is generated. 278 | #texinfo_domain_indices = True 279 | 280 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 281 | #texinfo_show_urls = 'footnote' 282 | 283 | 284 | # Example configuration for intersphinx: refer to the Python standard library. 285 | intersphinx_mapping = { 286 | 'python': ('https://docs.python.org/3/', None), 287 | 'sigtools': ('https://sigtools.readthedocs.io/en/latest/', None), 288 | 'sphinx': ('https://www.sphinx-doc.org/en/stable/', None), 289 | } 290 | 291 | autoclass_content = 'both' 292 | autodoc_member_order = 'bysource' 293 | autodoc_default_flags = ['members', 'undoc-members'] 294 | 295 | rst_prolog = """ 296 | 297 | .. |nbsp| unicode:: 0xA0 298 | :trim: 299 | 300 | .. |examples_url| replace:: examples 301 | .. _examples_url: https://github.com/epsy/clize/tree/master/examples 302 | 303 | .. 304 | 305 | """ 306 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. |on github| replace:: on GitHub 2 | .. _on github: https://github.com/epsy/clize/issues 3 | 4 | .. _contributing: 5 | 6 | Contributing 7 | ============ 8 | 9 | Thanks for considering helping out. We don't bite. :-) 10 | 11 | 12 | .. _bug report: 13 | 14 | Reporting issues 15 | ---------------- 16 | 17 | Bugs and other tasks are tracked |on github|. 18 | 19 | * Check whether the issue exists already. (Be sure to also check closed ones.) 20 | * Report which version of Clize the issue appears on. You can obtain it using:: 21 | 22 | pip show clize 23 | 24 | For documentation-related bugs, you can either look at the version in the 25 | page URL, click the "Read the docs" insigna in the bottom-left corner or the 26 | hamburger menu on mobile. 27 | * When applicable, show how to trigger the bug and what was expected instead. 28 | Writing a testcase for it is welcome, but not required. 29 | 30 | 31 | .. _submit patch: 32 | 33 | Submitting patches 34 | ------------------ 35 | 36 | Patches are submitted for review through GitHub pull requests. 37 | 38 | * Follow :pep:`8`. 39 | * When fixing a bug, include a test case in your patch. Make sure correctly 40 | tests against the bug: It must fail without your fix, and succeed with it. 41 | See :ref:`running tests`. 42 | * Submitting a pull request on GitHub implies your consent for merging, 43 | therefore authorizing the maintainer(s) to distribute your modifications 44 | under the project's license. 45 | 46 | 47 | .. _new features: 48 | 49 | Implementing new features 50 | ------------------------- 51 | 52 | Before implementing a new feature, please open an issue |on github| to discuss 53 | it. This ensures you do not work on a feature that would be refused inclusion. 54 | 55 | Add tests for your feature to the test suite and make sure it :ref:`completes 56 | on all supported versions of Python `. Make sure it is fully 57 | tested using the ``cover`` target. 58 | 59 | Feel free to submit a pull request as soon as you have changes you need 60 | feedback on. In addition, TravisCI will run the test suite on all supported 61 | platforms and will perform coverage checking for you on the pull request page. 62 | 63 | 64 | .. _running tests: 65 | 66 | Running the test suite 67 | ---------------------- 68 | 69 | The test suite can be run across all supported versions using, ``tox``:: 70 | 71 | pip install --user tox 72 | tox 73 | 74 | If you do not have all required Python versions installed or wish to save time 75 | when testing you can specify one version of Python to test against:: 76 | 77 | tox -e pyXY 78 | 79 | Where ``X`` and ``Y`` designate a Python version as in ``X.Y``. For instance, 80 | the following command runs the test suite against Python 3.4 only:: 81 | 82 | tox -e py34 83 | 84 | Branches linked in a pull request will be run through the test suite on 85 | TravisCI and the results are linked back in the pull request. Feel free to do 86 | this if installing all supported Python versions is impractical for you. 87 | 88 | `coverage.py `_ is used to measure 89 | code coverage. New code is expected to have full code coverage. You can run the 90 | test suite through it using:: 91 | 92 | tox -e cover 93 | 94 | It will print the measured code coverage and generate webpages with 95 | line-by-line coverage information in ``htmlcov``. Note that the ``cover`` 96 | target requires Python 3.4 or greater. 97 | 98 | 99 | .. _generating docs: 100 | 101 | Documentation 102 | ------------- 103 | 104 | The documentation is written using `sphinx `_ and lives 105 | in ``docs/`` from the project root. It can be built using:: 106 | 107 | tox -e docs 108 | 109 | This will produce documentation in ``build/sphinx/html/``. Note that Python 3.4 110 | must be installed to build the documentation. 111 | -------------------------------------------------------------------------------- /docs/dispatching.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: clize 2 | 3 | .. _dispatching: 4 | 5 | Dispatching to multiple functions 6 | ================================= 7 | 8 | 9 | So far the previous part of the tutorial showed you how to use clize to 10 | :ref:`run a single function `. Sometimes your program will need to 11 | perform different related actions that involve different parameters. For 12 | instance, `git `_ offers all kinds of commands related to 13 | managing a versioned code repository under the ``git`` command. Alternatively, 14 | your program could have one main function and a few auxiliary ones, for 15 | instance for verifying the format of a config file, or simply for displaying 16 | the program's version. 17 | 18 | 19 | .. _alternate commands: 20 | 21 | Alternate actions 22 | ----------------- 23 | 24 | These allow you to provide auxiliary functions to your program while one 25 | remains the main function. Let's write a program with an alternate command 26 | triggered by ``--version`` that prints the version. 27 | 28 | 29 | Here are the two functions we could have: ``do_nothing`` will be the main function while ``version`` will be provided as an alternate command. 30 | 31 | .. literalinclude:: /../examples/altcommands.py 32 | :lines: 4-14 33 | 34 | You use `run` as usual for the main function, but specify the alternate command 35 | in the ``alt=`` parameter: 36 | 37 | .. literalinclude:: /../examples/altcommands.py 38 | :lines: 1-3, 17 39 | 40 | The ``version`` function will be available as ``--version``: 41 | 42 | .. code-block:: console 43 | 44 | $ python3 examples/altcommands.py --help 45 | Usage: examples/altcommands.py 46 | 47 | Does nothing 48 | 49 | Other actions: 50 | -h, --help Show the help 51 | --version Show the version 52 | 53 | You can specify more alternate commands in a list. For instance, 54 | 55 | .. code-block:: python 56 | 57 | def build_date(*, show_time=False): 58 | """Show the build date for this version""" 59 | print("Build date: 17 August 1979", end='') 60 | if show_time: 61 | print(" afternoon, about tea time") 62 | print() 63 | 64 | 65 | run(do_nothing, alt=[version, build_date]) 66 | 67 | 68 | You can instead use a `dict` to specify their names if those automatically 69 | drawn from the function names don't suit you: 70 | 71 | .. code-block:: python 72 | 73 | run(do_nothing, alt={ 74 | 'totally-not-the-version': version, 75 | 'birthdate': build_date 76 | }) 77 | 78 | .. code-block:: console 79 | 80 | $ python3 examples/altcommands.py --help 81 | Usage: examples/altcommands.py 82 | 83 | Does nothing 84 | 85 | Other actions: 86 | --birthdate Show the build date for this version 87 | -h, --help Show the help 88 | --totally-not-the-version 89 | Show the version 90 | 91 | Using a `collections.OrderedDict` instance rather than `dict` will guarantee 92 | the order they appear in the help is the same as in the source. 93 | 94 | 95 | .. _multiple commands: 96 | 97 | Multiple commands 98 | ----------------- 99 | 100 | This allows you to keep multiple commands under a single program without 101 | singling one out as the main one. They become available by naming the 102 | subcommand directly after the program's name on the command line. 103 | 104 | Let's see how we can use it in a mock todo list application: 105 | 106 | .. literalinclude:: /../examples/multicommands.py 107 | :lines: 4-14 108 | 109 | You can specify multiple commands to run by passing each function as an 110 | argument to `.run`: 111 | 112 | .. code-block:: python 113 | 114 | from clize import run 115 | 116 | 117 | run(add, list_) 118 | 119 | 120 | .. code-block:: console 121 | 122 | $ python3 examples/multicommands.py add A very important note. 123 | OK I will remember that. 124 | $ python3 examples/multicommands.py list 125 | Sorry I forgot it all :( 126 | 127 | Alternatively, as with :ref:`alternate commands `, you can 128 | pass in an :term:`python:iterable` of functions, a `dict` or an 129 | `~collections.OrderedDict`. If you pass an iterable of functions, :ref:`name 130 | conversion ` will apply. 131 | 132 | .. code-block:: python 133 | 134 | run({ 'add': add, 'list': list_, 'show': list_ }) 135 | 136 | Because it isn't passed a regular function with a docstring, Clize can't 137 | determine an appropriate description from a docstring. You can explicitly give 138 | it a description with the ``description=`` parameter. Likewise, you an add 139 | footnotes with the ``footnotes=`` parameter. The format is the same as with 140 | other docstrings, just without documentation for parameters. 141 | 142 | .. literalinclude:: /../examples/multicommands.py 143 | :lines: 17-21 144 | 145 | .. code-block:: console 146 | 147 | $ python3 examples/multicommands.py --help 148 | Usage: examples/multicommands.py command [args...] 149 | 150 | A reliable to-do list utility. 151 | 152 | Store entries at your own risk. 153 | 154 | Commands: 155 | add Adds an entry to the to-do list. 156 | list Lists the existing entries. 157 | 158 | Often, you will need to share a few characteristics, for instance a set of 159 | parameters, between multiple functions. See how Clize helps you do that in 160 | :ref:`function compositing`. 161 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _faq: 3 | 4 | Frequently asked questions 5 | ========================== 6 | 7 | Drafts! These docs are all new and the FAQ is almost empty! Help me improve this 8 | dire situation by :ref:`contacting me ` with your questions! 9 | 10 | .. contents:: Table of contents 11 | :local: 12 | :backlinks: none 13 | 14 | .. _python version: 15 | .. _python versions: 16 | 17 | What versions of Python are supported? 18 | -------------------------------------- 19 | 20 | Clize is tested to run successfully on Python 3.6 through 3.10. 21 | 22 | For other Python versions: 23 | 24 | ============== ========================================================== 25 | Python version Last compatible version of Clize 26 | ============== ========================================================== 27 | Python 2.6 `Clize 3.1 `_ 28 | Python 2.7 `Clize 4.2 `_ 29 | Python 3.3 `Clize 4.0 `_ 30 | Python 3.4 `Clize 4.1 `_ 31 | Python 3.5 `Clize 4.2 `_ 32 | ============== ========================================================== 33 | 34 | 35 | .. _dependencies: 36 | 37 | What libraries are required for Clize to run? 38 | --------------------------------------------- 39 | 40 | Using ``pip`` to install Clize from PyPI as in :ref:`install` will 41 | automatically install the right dependencies for you. 42 | 43 | If you still need the list, Clize always requires: 44 | 45 | * `sigtools `_: 46 | Utilities to help manipulate function signatures. 47 | * `od `_: Shorthand for OrderedDict. 48 | * `attrs `_: Classes without boilerplate. 49 | * `docutils `_: To parse docstrings. 50 | 51 | If you wish to use `clize.converters.datetime`, you need: 52 | 53 | * `python-dateutil `_: For 54 | parsing dates. 55 | 56 | ``pip`` will install ``dateutil`` if you specify to install Clize with the 57 | ``datetime`` option, i.e. ``pip install "clize[datetime]"``. 58 | 59 | .. _ancient pip: 60 | 61 | I just installed Clize using ``pip`` and I still get ``ImportErrors`` 62 | --------------------------------------------------------------------- 63 | 64 | Old versions of ``pip`` do not read Python-version dependent requirements and 65 | therefore do not install ``funcsigs`` or ``ordereddict``. To remedy this, you can: 66 | 67 | * Upgrade ``pip`` and :ref:`install Clize ` again. (Use the ``-U`` flag of ``pip 68 | install`` to force a reinstall.) 69 | * Install the :ref:`dependencies ` manually. 70 | 71 | 72 | .. _sigtools split: 73 | 74 | What is ``sigtools`` and why is it a separate library? 75 | ------------------------------------------------------ 76 | 77 | `sigtools` is used in many of the examples throughout this documentation, and 78 | it is maintained by the same person as Clize, thus the above question. 79 | 80 | Clize's purpose is twofold: 81 | 82 | * Convert the idioms of a function signature into those of a CLI, 83 | * Parse the input that CLI arguments are. 84 | 85 | It turns out that just asking for the function signature from 86 | `inspect.signature` is not good enough: 87 | 88 | * Python 2 syntax, which was supported at the time, 89 | cannot be used to express keyword-only parameters. 90 | * `inspect.signature` cannot process decorators that return a function with 91 | slightly altered parameters. 92 | 93 | For the first point, Clize could have accepted an argument that said "do as if 94 | that parameter was keyword-only and make it a named parameter on the CLI" (and 95 | in fact it used to), but that would have Clize behave according to a signature 96 | *and a bunch of things around it*, which is a concept it tries to steer away 97 | from. 98 | 99 | For the second, some tooling would be necessary to specify how exactly a 100 | decorator affected a wrapped function's parameters. 101 | 102 | Modifying and making signatures more useful was both complex and independent 103 | from command-line argument parsing, so it was made a separate library as 104 | `sigtools`. 105 | 106 | So there you have it, `sigtools` helps you add keyword-only parameters on 107 | Python 2, and helps decorators specify how they alter parameters on decorated 108 | functions. All Clize sees is the finished accurate signature from which it 109 | infers a CLI. 110 | 111 | 112 | .. _faq other parsers: 113 | 114 | What other libraries can be used for argument parsing? 115 | ------------------------------------------------------ 116 | 117 | See :ref:`clize alternatives`. 118 | 119 | 120 | .. _faq mutual exclusive flag: 121 | 122 | How can I write mutually exclusive flags? 123 | ----------------------------------------- 124 | 125 | Mutually exclusive flags refer to when a user can use one flag A (``--flag-a``) 126 | or the other (``--flag-b``), but not both at the same time. 127 | 128 | It is a feature that is difficult to express in a function signature as well as 129 | on the ``--help`` screen for the user (other than in the full usage form). 130 | It is therefore recommended to use a positional parameter or option that 131 | accepts one of specific values. `~clize.parameters.one_of` can help you do 132 | that. 133 | 134 | If you still think mutually exclusive parameters are your best option, you can 135 | check for the condition in your function and raise `clize.ArgumentError`, as in 136 | the :ref:`arbitrary requirements` part of the tutorial. 137 | 138 | 139 | .. index:: DRY 140 | .. _faq share features: 141 | 142 | Some of my commands share features, can I reuse code somehow? 143 | ------------------------------------------------------------- 144 | 145 | Yes! You can use decorators much like in regular Python code, see 146 | :ref:`function compositing`. 147 | 148 | 149 | .. _get more help: 150 | 151 | Where can I find more help? 152 | --------------------------- 153 | 154 | You can get help by :ref:`contacting me directly `, writing in the dedicated `Gitter chatroom `_, using the `#clize 155 | #python hashtags on Twitter 156 | `_, or by posting 157 | in the `Clize Google+ 158 | community `_. 159 | 160 | .. _contact: 161 | 162 | Contacting the author 163 | --------------------- 164 | 165 | You can contact me via `@YannKsr on Twitter `_ or 166 | via `email `_. Feel free to ask about Clize! 167 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. _history: 2 | 3 | An illustrated history 4 | ---------------------- 5 | 6 | This document recounts the story of each clize version. Here's the short 7 | version first: 8 | 9 | ======= =================== ======================================= 10 | Version Release date Major changes 11 | ======= =================== ======================================= 12 | 1.0b April 4th 2011 First release 13 | 2.0 October 7th 2012 Subcommands, Python 3 syntax support 14 | 2.2 August 31st 2013 Minor additions 15 | 2.4 October 2nd 2013 Bugfixes 16 | 3.0 May 13th 2015 Extensibility, decorators, focus on py3 17 | 3.1 October 3rd 2016 Better decorators, py3-first docs 18 | ======= =================== ======================================= 19 | 20 | You can also browse the :ref:`release notes `. 21 | 22 | And here's the story: 23 | 24 | 25 | .. _before clize: 26 | 27 | Before Clize 28 | ............ 29 | 30 | .. |twopt| replace:: twisted's `usage.Options` 31 | .. _twopt: http://twistedmatrix.com/documents/13.1.0/core/howto/options.html 32 | 33 | After having used `optparse` and |twopt|_ in various medium-sized projects, and 34 | viewing `argparse` as being more of the same, I wondered if I needed all this 35 | when writing a trivial program. I realized that if I only wanted to read some 36 | positional arguments, I could just use tuple unpacking on `sys.argv`: 37 | 38 | .. code-block:: python 39 | 40 | from __future__ import print_function 41 | 42 | import sys 43 | 44 | 45 | script, first, last = sys.argv 46 | print('Hello', first, last) 47 | 48 | If you used argument packing in a function call instead, you gain the ability to 49 | make use of default values: 50 | 51 | .. code-block:: python 52 | 53 | from __future__ import print_function 54 | 55 | import sys 56 | 57 | 58 | def greet(script, first, last, greeting="Hello"): 59 | print(greeting, first, last) 60 | 61 | 62 | greet(*sys.argv) 63 | 64 | It works nicely save for the fact that you can't request a help page from it or 65 | have named options. So I set out to add those capabilities while doing my best 66 | to keep it as simple as possible, like the above example. 67 | 68 | 69 | .. _first release: 70 | 71 | 1.0: A first release 72 | .................... 73 | 74 | .. code-block:: python 75 | 76 | from __future__ import print_function 77 | 78 | from clize import clize, run 79 | 80 | 81 | @clize 82 | def greet(first, last, greeting="Hello"): 83 | print(greeting, first, last) 84 | 85 | 86 | run(greet) 87 | 88 | Thanks to the ability in Python to look at a function's signature, you gained a 89 | ``--help`` page, and ``greeting`` was available as ``--greeting`` on the 90 | command line, while adding just one line of code. This was very different from 91 | what `argparse` had to offer. It allowed you to almost completely ignore 92 | argument parsing and just write your program's logic as a function, with your 93 | parameters' documented in the docstring. 94 | 95 | In a way, Clize had opinions about what a CLI should and shouldn't be like. For 96 | instance, it was impossible for named parameters to be required. It was 97 | generally very rigid, which was fine given its purpose of serving smaller 98 | programs. 99 | 100 | It hadn't visibly garnered much interest. I was still a user myself, and no 101 | other argument parser had really interested me, so I kept using it and watched 102 | out for possible improvements. Aside from the subcommand dispatcher, there was 103 | little user feedback so the inspiration ended up coming from somewhere else. 104 | 105 | 106 | .. _history annotations: 107 | 108 | 2.0: Function annotations 109 | ......................... 110 | 111 | Clize 2.0 came out with two major features. :ref:`Subcommands ` and a new way of specifying additional information on the 113 | parameters. I'll skip over subcommands because they are already a well 114 | established concept in argument parsing. See :ref:`multiple commands` for their 115 | documentation. 116 | 117 | Through now forgotten circumstances, I came across :pep:`3107` implemented 118 | since Python 3.0, which proposed a syntax for adding information about 119 | parameters. 120 | 121 | Up until then, if you wanted to add an alias to a named parameter, it looked a bit like this: 122 | 123 | .. code-block:: python 124 | 125 | from __future__ import print_function 126 | 127 | from clize import clize, run 128 | 129 | 130 | @clize(require_excess=True, aliases={'reverse': ['r']}) 131 | def echo(reverse=False, *args): 132 | text = ' '.join(args) 133 | if reverse: 134 | text = text[::-1] 135 | print(text) 136 | 137 | 138 | run(echo) 139 | 140 | Many things involved passing parameters in the decorator. It was generally 141 | quite ugly, especially when more than one parameter needed adjusting, at which 142 | point the decorator call grew to the point of needing to be split over multiple 143 | lines. 144 | 145 | The parameter annotation syntax from :pep:`3107` was fit to replace this. You 146 | could tag the parameter directly with the alias or conversion function or 147 | whatever. It involved looking at the type of each annotation, but it was a lot 148 | more practical than spelling *alias*, *converter* and the parameter's name all 149 | over the place. 150 | 151 | It also allowed for keyword-only parameters from :pep:`3102` to map directly to 152 | named parameters while others would always be positional parameters. 153 | 154 | .. code-block:: python 155 | 156 | from __future__ import print_function 157 | 158 | from clize import clize, run 159 | 160 | 161 | @clize(require_excess=True) 162 | def echo(*args, reverse:'r'=False): 163 | text = ' '.join(args) 164 | if reverse: 165 | text = text[::-1] 166 | print(text) 167 | 168 | 169 | run(echo) 170 | 171 | Python 3 wasn't quite there yet, so these were just features on the side at the 172 | time. I liked it a lot however and used it whenever I could, but had to use the 173 | older interface whenever I had to use Python 2. 174 | 175 | 176 | .. _history rewrite: 177 | 178 | 3.0: The rewrite 179 | ................ 180 | 181 | Python 3.3 introduced `inspect.signature`, an alternative to the rough 182 | `inspect.getfullargspec`. This provided an opportunity to start again from 183 | scratch to build something on a solid yet flexible base. 184 | 185 | For versions of Python below 3.3, a backport of `inspect.signature` existed on 186 | `PyPI `_. This inspired a Python 3-first approach: The 187 | old interface was deprecated in favor of the one described just above. 188 | 189 | .. code-block:: python 190 | 191 | from clize import run, parameter 192 | 193 | def echo(*args: parameter.required, reverse:'r'=False): 194 | text = ' '.join(args) 195 | if reverse: 196 | text = text[::-1] 197 | print(text) 198 | 199 | run(echo) 200 | 201 | Since the ``@clize`` decorator is gone, ``echo`` is now just a regular function 202 | that could theoretically be used in non-cli code or tests. 203 | 204 | Users looking to keep Python 2 compatibility would have to use a compatibility 205 | layer for keyword-only parameters and annotations: `sigtools.modifiers`. 206 | 207 | .. code-block:: python 208 | 209 | from __future__ import print_function 210 | 211 | from sigtools import modifiers 212 | from clize import run, parameter 213 | 214 | @modifiers.autokwoargs 215 | @modifiers.annotate(args=parameter.REQUIRED, reverse='r') 216 | def echo(reverse=False, *args): 217 | text = ' '.join(args) 218 | if reverse: 219 | text = text[::-1] 220 | print(text) 221 | 222 | run(echo) 223 | 224 | 225 | `sigtools` was created specifically because of Clize, but it aims to be a 226 | generic library for manipulating function signatures. Because of Clize's 227 | reliance on accurate introspection data on functions and callables in general, 228 | `sigtools` also provided tools to fill the gap when `inspect.signature` 229 | stumbles. 230 | 231 | For instance, when a decorator replaces a function and complements its 232 | parameters, `inspect.signature` would only produce something like ``(spam, 233 | *args, ham, **kwargs)`` when Clize would need more information about what 234 | ``*args`` and ``**kwargs`` mean. 235 | 236 | `sigtools` thus provided decorators such as `~sigtools.specifiers.forwards` and 237 | the higher-level `~sigtools.wrappers.wrapper_decorator` for specifying what 238 | these parameters meant. This allowed for :ref:`creating decorators for CLI 239 | functions ` in a way analogous to regular decorators, 240 | which was up until then something other introspection-based tools had never 241 | done. It greatly improved Clize's usefulness with multiple commands. 242 | 243 | With the parser being completely rewritten, a large part of the argument 244 | parsing was moved away from the monolithic "iterate over `sys.argv`" loop to 245 | one that deferred much of the behaviour to parameter objects determined from 246 | the function signature. This allows for library and application authors to 247 | almost completely :ref:`customize how their parameters work `, including things like replicating ``--help``'s behavior of working 249 | even if there are errors beforehand, or other completely bizarre stuff. 250 | 251 | This is a departure from Clize's opiniated beginnings, but the defaults remain 252 | sane and it usually takes someone to create new `~clize.parser.Parameter` 253 | subclasses for bizarre stuff to be made. In return Clize gained a flexibility 254 | few other argument parsers offer. 255 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ************************************************** 2 | clize: Turn functions into command-line interfaces 3 | ************************************************** 4 | 5 | Clize is an argument parser for `Python `_. You can 6 | use Clize as an alternative to ``argparse`` if you want an even easier way to 7 | create command-line interfaces. 8 | 9 | .. rubric:: With Clize, you can: 10 | 11 | * Create command-line interfaces by creating functions and passing them to 12 | `clize.run`. 13 | * Enjoy a CLI automatically created from your functions' parameters. 14 | * Bring your users familiar ``--help`` messages generated from your docstrings. 15 | * Reuse functionality across multiple commands using decorators. 16 | * Extend Clize with new parameter behavior. 17 | 18 | Here's an example: 19 | 20 | .. literalinclude:: /../examples/hello.py 21 | :emphasize-lines: 3,16 22 | 23 | `~clize.run` takes the function and automatically produces a command-line 24 | interface for it: 25 | 26 | .. code-block:: console 27 | 28 | $ python3 -m pip install --user clize 29 | $ python3 examples/hello.py --help 30 | Usage: examples/hello.py [OPTIONS] [name] 31 | 32 | Greets the world or the given name. 33 | 34 | Positional arguments: 35 | name If specified, only greet this person. 36 | 37 | Options: 38 | --no-capitalize Don't capitalize the give name. 39 | 40 | Other actions: 41 | -h, --help Show the help 42 | $ python hello.py 43 | Hello world! 44 | $ python hello.py john 45 | Hello John! 46 | $ python hello.py dave --no-capitalize 47 | Hello dave! 48 | 49 | .. rubric:: Interested? 50 | 51 | * Follow the :ref:`tutorial ` 52 | * Browse the |examples_url|_ 53 | * Ask for help `on Gitter `_ 54 | * Check out :ref:`why Clize was made ` 55 | * Star, watch or fork `Clize on GitHub `_ 56 | 57 | .. only:: html 58 | 59 | Here is the full table of contents: 60 | 61 | 62 | .. _about: 63 | 64 | .. only:: latex 65 | 66 | .. toctree:: 67 | :caption: Clize documentation 68 | 69 | .. raw:: latex 70 | 71 | \part{About Clize} 72 | 73 | .. toctree:: 74 | :maxdepth: 1 75 | :caption: About Clize 76 | 77 | why 78 | alternatives 79 | history 80 | faq 81 | 82 | 83 | .. _tutorial: 84 | 85 | .. raw:: latex 86 | 87 | \part{Getting started} 88 | 89 | .. toctree:: 90 | :caption: Getting started 91 | 92 | basics 93 | dispatching 94 | compositing 95 | 96 | 97 | .. _guides: 98 | 99 | .. raw:: latex 100 | 101 | \part{Guides} 102 | 103 | .. toctree:: 104 | :caption: Guides 105 | 106 | parser 107 | porting 108 | interop 109 | 110 | 111 | .. _references: 112 | 113 | .. raw:: latex 114 | 115 | \part{Reference} 116 | 117 | The user reference lists all capabilities of each kind of parameter. The API reference comes in handy if you're extending clize. 118 | 119 | .. toctree:: 120 | :caption: Reference 121 | 122 | reference 123 | docstring-reference 124 | api 125 | 126 | .. _project doc: 127 | 128 | .. raw:: latex 129 | 130 | \part{Project documentation} 131 | 132 | Information on how Clize is organized as a project. 133 | 134 | .. toctree:: 135 | :caption: Project documentation 136 | 137 | releases 138 | contributing 139 | -------------------------------------------------------------------------------- /docs/interop.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: clize 2 | 3 | .. _interop: 4 | 5 | Interoperability with arbitrary callables 6 | ----------------------------------------- 7 | 8 | Clize operates as a callable that takes each item from `sys.argv` or something 9 | supposed to replace it. It is therefore easy to substitute it with another 10 | callable that has such parameters. 11 | 12 | 13 | .. _interop no inference: 14 | 15 | Avoiding automatic CLI inference 16 | ................................ 17 | 18 | When an object is passed to run, either as sole command, one in many 19 | subcommands or as an alternative action, it attempts to make a :ref:`CLI object 20 | ` out of it if it isn't one already. It simply checks if there is a 21 | ``cli`` attribute and uses it, or it wraps it with `.Clize`. 22 | 23 | To insert an arbitrary callable, you must therefore place it as the ``cli`` 24 | attribute of whatever object you pass to `.run`. 25 | 26 | `clize.Clize.as_is` does exactly that. You can use it as a decorator or when 27 | passing it to run: 28 | 29 | .. code-block:: python 30 | 31 | import argparse 32 | 33 | from clize import Clize, parameters, run 34 | 35 | 36 | @Clize.as_is 37 | def echo_argv(*args): 38 | print(*args, sep=' | ') 39 | 40 | 41 | def using_argparse(name: parameters.pass_name, *args): 42 | parser = argparse.ArgumentParser(prog=name) 43 | parser.add_argument('--ham') 44 | ns = parser.parse_args(args=args) 45 | print(ns.ham) 46 | 47 | 48 | run(echo_argv, Clize.as_is(using_argparse)) 49 | 50 | .. code-block:: console 51 | 52 | $ python interop.py echo-argv ab cd ef 53 | interop.py echo-argv | ab | cd | ef 54 | $ python interop.py using-argparse --help 55 | usage: interop.py using-argparse [-h] [--ham HAM] 56 | 57 | optional arguments: 58 | -h, --help show this help message and exit 59 | --ham HAM 60 | $ python interop.py using-argparse spam 61 | usage: interop.py using-argparse [-h] [--ham HAM] 62 | interop.py using-argparse: error: unrecognized arguments: spam 63 | $ python interop.py using-argparse --ham spam 64 | spam 65 | 66 | 67 | .. _interop description: 68 | 69 | Providing a description in the parent command's help 70 | .................................................... 71 | 72 | If you try to access the above program's help screen, Clize will just leave the 73 | description for each external command empty: 74 | 75 | .. code-block:: console 76 | 77 | : .tox/docs/bin/python interop.py --help 78 | Usage: interop.py command [args...] 79 | 80 | Commands: 81 | echo-argv 82 | using-argparse 83 | 84 | Clize expects to find a description as ``cli.helper.description``. You can 85 | either create an object there or let `Clize.as_is` do it for you: 86 | 87 | .. code-block:: python 88 | 89 | @Clize.as_is(description="Prints argv separated by pipes") 90 | def echo_argv(*args): 91 | print(*args, sep=' | ') 92 | 93 | ... 94 | 95 | run(echo_argv, 96 | Clize.as_is(using_argparse, 97 | description="Prints the value of the --ham option")) 98 | 99 | .. code-block:: console 100 | 101 | $ python interop.py --help 102 | Usage: interop.py command [args...] 103 | 104 | Commands: 105 | echo-argv Prints argv separated by pipes 106 | using-argparse Prints the value of the --ham option 107 | 108 | 109 | .. _interop usage: 110 | 111 | Advertising command usage 112 | ......................... 113 | 114 | To produce the ``--help --usage`` output, Clize uses ``cli.helper.usages()`` to 115 | produce an iterable of ``(command, usage)`` pairs. When it can't determine it, 116 | it shows a generic usage signature instead. 117 | 118 | You can use `Clize.as_is`'s ``usages=`` parameter to provide it: 119 | 120 | .. code-block:: python 121 | 122 | run(echo_argv, 123 | Clize.as_is(using_argparse, 124 | description="Prints the value of the --ham option"), 125 | usages=['--help', '[--ham HAM]']) 126 | 127 | .. code-block:: console 128 | 129 | $ python interop.py --help --usage 130 | interop.py --help [--usage] 131 | interop.py echo-argv [args...] 132 | interop.py using-argparse --help 133 | interop.py using-argparse [--ham HAM] 134 | 135 | The example is available as ``examples/interop.py``. 136 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\clize.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\clize.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/porting.rst: -------------------------------------------------------------------------------- 1 | Upgrading from older releases 2 | ============================= 3 | 4 | This document will instruct you in porting applications using older clize versions to the newest version. 5 | 6 | .. _porting-2: 7 | 8 | Upgrading from clize 1 and 2 9 | ---------------------------- 10 | 11 | Clize 3 now only treats keyword-only parameters on the function as named 12 | parameters and does not convert any parameter from keyword to positional or 13 | vice-versa, much like when the ``use_kwoargs`` parameter is used in Clize 2. 14 | Aliases, and other parameter-related information are now expressed exclusively 15 | through parameter annotations. 16 | 17 | However, `clize.clize` is provided: it imitates the old behaviour but adds a 18 | deprecation warning when used. 19 | 20 | 21 | .. _porting clize decorator: 22 | 23 | Porting code using the ``@clize`` decorator with no arguments 24 | _____________________________________________________________ 25 | 26 | Consider this code made to work with Clize 1 or 2:: 27 | 28 | from clize import clize, run 29 | 30 | @clize 31 | def func(positional, option=3): 32 | pass # ... 33 | 34 | if __name__ == '__main__': 35 | run(func) 36 | 37 | Here, you can drop the ``@clize`` line completely, but you have to convert 38 | ``option`` to a keyword-only parameter:: 39 | 40 | from clize import run 41 | 42 | def func(positional, *, option=3): 43 | pass # ... 44 | 45 | if __name__ == '__main__': 46 | run(func) 47 | 48 | 49 | .. _porting force_positional: 50 | 51 | ``force_positional`` 52 | ____________________ 53 | 54 | ``force_positional`` used to let you specify parameters with defaults that you 55 | don't want as named options:: 56 | 57 | from clize import clize, run 58 | 59 | @clize(force_positional=['positional_with_default']) 60 | def func(positional, positional_with_default=3, option=6): 61 | pass # ... 62 | 63 | if __name__ == '__main__': 64 | run(func) 65 | 66 | This issue isn't relevant anymore as keyword-only arguments are explicitly 67 | specified. 68 | 69 | If you're using `~sigtools.modifiers.autokwoargs`, the ``exceptions`` parameter 70 | can prevent parameters from being converted:: 71 | 72 | from sigtools.modifiers import autokwoargs 73 | from clize import run 74 | 75 | @autokwoargs(exceptions=['positional_with_default']) 76 | def func(positional, positional_with_default=3, option=6): 77 | pass # ... 78 | 79 | if __name__ == '__main__': 80 | run(func) 81 | 82 | 83 | .. _porting alias: 84 | .. _porting coerce: 85 | 86 | Porting code that used ``alias`` or ``coerce`` 87 | ______________________________________________ 88 | 89 | The ``alias`` and ``coerce`` were used in order to specify alternate names for 90 | options and functions to convert the value of arguments, respectively:: 91 | 92 | from clize import clize, run 93 | 94 | @clize( 95 | alias={'two': ['second'], 'three': ['third']}, 96 | coerce={'one': int, 'three': int}) 97 | def func(one, two=2, three=None): 98 | ... 99 | 100 | if __name__ == '__main__': 101 | run(func) 102 | 103 | You now pass these as annotations on the corresponding parameter:: 104 | 105 | from clize import run 106 | 107 | def func(one:int, *, two='second', three:int='third')): 108 | ... 109 | 110 | if __name__ == '__main__': 111 | run(func) 112 | 113 | 114 | .. _porting require_excess: 115 | 116 | ``require_excess`` 117 | __________________ 118 | 119 | Indicating that an ``*args``-like parameter is required is now done by 120 | annotating the parameter with `Parameter.REQUIRED 121 | ` or `Parameter.R` for short:: 122 | 123 | from clize import run, Parameter 124 | 125 | def func(*args:Parameter.R): 126 | pass # ... 127 | 128 | if __name__ == '__main__': 129 | run(func) 130 | 131 | 132 | .. _porting make_flag: 133 | 134 | ``extra`` and ``make_flag`` 135 | ___________________________ 136 | 137 | Alternate actions as shown in Clize 2's tutorial are now done by passing the 138 | function directly to `.run` :ref:`as shown in the tutorial `. Unlike previously, the alternate command function is passed to the 140 | clizer just like the main one. 141 | 142 | For other use cases, you should find the appropriate parameter class from 143 | `clize.parser` or subclass one, instantiate it and pass it in a sequence as the 144 | ``extra`` parameter of `.Clize` or `.run`. If the parameter matches one 145 | actually present on the source function, annotate that parameter with your 146 | `.Parameter` instance instead of passing it to ``extra``. 147 | -------------------------------------------------------------------------------- /docs/releases.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: clize 2 | 3 | .. _releases: 4 | 5 | Release notes 6 | ============= 7 | 8 | .. _v5.0.1: 9 | 10 | 5.0.1 (2023-07-06) 11 | ------------------ 12 | 13 | * Fixed ``run(..., alt={...})`` not working correctly with dicts 14 | 15 | .. _v5.0: 16 | .. _v5.0.0: 17 | 18 | 5.0.0 (2022-10-13) 19 | ------------------ 20 | 21 | Breaking changes: 22 | 23 | * `bytes` annotation (converter) now re-encodes the corresponding parameter back to bytes 24 | 25 | Deprecations: 26 | 27 | * The ``convert_default`` argument to `~clize.parser.value_converter` is now deprecated. 28 | `Parameter.cli_default` is the recommended replacement. 29 | To help transition, a new ``convert_default_filter`` option has been added to `~clize.parser.value_converter`, 30 | which allows converter authors to filter which values are selected for default conversion, 31 | thereby not incurring a deprecation warning for the user. 32 | 33 | Updated compatibility: 34 | 35 | * Dropped support for Python 2.7, Python 3.5. 36 | * Verified support for Python 3.10+ 37 | * Compatibility with Docutils 0.21+ 38 | * Improved path support for Windows 39 | 40 | Improvements: 41 | 42 | * Automatically group identical subcommands as aliases 43 | * Evaluate deferred annotations from :pep:`563` 44 | * Basic support for `typing.Annotated` 45 | * `Parameter.cli_default` allows script writers to specify a default value 46 | that will be converted like an argument passed in from the CLI, 47 | but not supplied when the function is called from Python. 48 | 49 | Process updates: 50 | 51 | * Updated Continuous Integration and Delivery to speed up releases and move off of deprecated CI platform. 52 | 53 | .. _v4.2: 54 | 55 | .. _v4.2.1: 56 | 57 | 4.2.1 (2021-11-13) 58 | ------------------ 59 | 60 | * Fixed build dependencies for documentation generation 61 | 62 | .. _v4.2.0: 63 | 64 | 4.2.0 (2021-06-29) 65 | ------------------ 66 | 67 | * Dropped support for Python 3.4. 68 | * Upgrade to attrs 21 69 | 70 | .. _v4.1: 71 | 72 | .. _v4.1.2: 73 | 74 | 4.1.2 (2021-11-13) 75 | ------------------ 76 | 77 | * Fixed build dependencies for documentation generation 78 | 79 | .. _v4.1.1: 80 | 81 | 4.1.1 (2019-10-17) 82 | ------------------ 83 | 84 | * Fix project description not appearing on PyPI. 85 | 86 | .. _v4.1.0: 87 | 88 | 4.1.0 (2019-10-17) 89 | ------------------ 90 | 91 | * Dropped support for Python 3.3. 92 | * Allow custom capitalization for named parameter aliases. 93 | * `pathlib.Path` is now automatically discovered as a value converter. 94 | * Fixed crash when using a Clize program across Windows drives. 95 | 96 | .. _v4.0: 97 | 98 | .. _v4.0.4: 99 | 100 | 4.0.4 (2021-11-13) 101 | ------------------ 102 | 103 | * Fixed build dependencies for documentation generation 104 | 105 | .. _v4.0.3: 106 | 107 | 4.0.3 (2018-02-01) 108 | ------------------ 109 | 110 | * Requires attrs >17.4.0 to fix a crash in the parser. 111 | 112 | .. _v4.0.2: 113 | 114 | 4.0.2 (2017-11-18) 115 | ------------------ 116 | 117 | * Fixed converted default arguments always overriding provided arguments. 118 | 119 | .. _v4.0.1: 120 | 121 | 4.0.1 (2017-04-2017) 122 | -------------------- 123 | 124 | * Fixed code blocks not displaying correctly in Sphinx docstrings. 125 | 126 | .. _v4.0.0: 127 | 128 | 4.0 (2017-04-19) 129 | ---------------- 130 | 131 | * Clize now parses Sphinx-style docstrings. It becomes the recommended way of 132 | documenting functions, as it is interoperable and not specific to Clize. 133 | * Value converters can now convert the default value for their parameter. 134 | Specify ``convert_default=True`` when decorating with 135 | `~clize.parser.value_converter`. 136 | * `clize.converters.file`: 137 | 138 | * you can use it without parenthesis now: ``def func(infile:converters.file):`` 139 | * it now converts the default parameter: ``infile:converters.file='-'`` 140 | gives a file opener for stdin if no value was provided by the user 141 | 142 | * `parameters.mapped`: Raises an error when two identical values are given. 143 | * Improved error messages for incorrect annotations. 144 | * Parameter converters must have a name. You can now specify one using the 145 | ``name=`` keyword argument to `~parser.parameter_converter`. 146 | * Clize now shows a hint if a subcommand is misspelled. 147 | * Dropped Python 2.6 support. Use Clize 3.1 if you need to support it. 148 | * Fix wrong docstring being used for header/footer text when the intended 149 | function had no (visible) parameters. 150 | * Extension API changes: 151 | 152 | * `parser.CliBoundArguments` now uses the ``attrs`` package. Instead of 153 | parsing arguments on instantiation, it has a process_arguments method for 154 | this. This is a breaking change if you were instantiating it directly 155 | rather than use `parser.CliSignature.read_arguments` 156 | * Separate the value setting logic in `~parser.ParameterWithValue` to a 157 | `~clize.parser.ParameterWithValue.set_value` method. Most parameter types 158 | don't need to override `~clize.parser.ParameterWithValue.read_argument` 159 | anymore. 160 | * Separated the help CLI from documentation generation and display. Also 161 | comes with more ``attrs``. This API is now documented. 162 | 163 | 164 | .. _v3.1: 165 | 166 | 3.1 (2016-10-03) 167 | ---------------- 168 | 169 | * Support for sigtools' automatic signature discovery. This is reflected 170 | in the function composition tutorial: In most cases you no longer have 171 | to specify how your decorators use `*args` and `**kwargs` exactly 172 | * Suggestions are provided when named parameters are misspelled. (Contributed 173 | by Karan Parikh.) 174 | * You can supply 'alternative actions' (i.e. --version) even when using 175 | multiple commands. 176 | * Improve hackability of argument parsing: named parameters are now sourced 177 | from the bound arguments instance, so a parameter could modify it duing 178 | parsing without changing the original signature. 179 | * Various documentation improvements. 180 | 181 | 182 | .. _v3.0: 183 | 184 | 3.0 (2015-05-13) 185 | ---------------- 186 | 187 | Version 3.0 packs a full rewrite. While it retains backwards-compatibility, the 188 | old interface is deprecated. See :ref:`porting-2`. 189 | 190 | * The argument parsing logic has been split between a loop over the parameters 191 | and parameter classes. New parameter classes can be made to implement cusom 192 | kinds of parameters. 193 | * The CLI inference is now based on `sigtools.specifiers.signature` rather than 194 | `inspect.getfullargspec`. This enables a common interface for the function 195 | signature to be manipulated prior to being passed to Clize. Namely, the 196 | ``__signature__`` attribute can be overridden or `sigtools`'s lazy 197 | `~sigtools.specifiers.forger_function` method can be employed. 198 | * The ``@clize`` decorator is deprecated in favor of directly passing functions 199 | to `~clize.run`, thus leaving the original function intact. 200 | * Named parameters are now obtained exclusively through keyword-only 201 | parameters. Other information about each parameter is communicated through 202 | parameter annotations. `sigtools.modifiers` provides backwards-compatibility 203 | for Python 2. 204 | * As a result of implementing the function signature-based abstraction, there 205 | are :ref:`ways to set up decorators that work with Clize `. 207 | * The help system now accepts :ref:`subsection headers for named parameters 208 | `. 209 | * *Coercion functions* have been renamed to *value converters*. Except for a few 210 | notable exceptions, they must be :ref:`tagged with a decorator `. This also applies to the type of supplied default values. 212 | * :ref:`Alternate actions ` (for instance ``--version``) can 213 | be supplied directly to run. 214 | * Several *Parameter converter* annotations have been added, including 215 | parameters with a limited choice of values, repeatable parameters, parameters 216 | that always supply the same value, and more. 217 | 218 | 219 | .. _v2.0: 220 | 221 | 2.0 (2012-10-07) 222 | ---------------- 223 | 224 | This release and earlier were documented post-release. 225 | 226 | Version 2.0 adds subcommands and support for function annotations and 227 | keyword-only parameters. 228 | 229 | 230 | .. _v1.0: 231 | 232 | 1.0 (2011-04-04) 233 | ---------------- 234 | 235 | Initial release. 236 | -------------------------------------------------------------------------------- /docs/why.rst: -------------------------------------------------------------------------------- 1 | .. _why: 2 | 3 | Why Clize was made 4 | ================== 5 | 6 | Clize started from the idea that other argument parsers were too complicated to 7 | use. Even for a small script, one would have to use a fairly odd interface to first generate a « parser » object with the correct behavior then use it. 8 | 9 | .. code-block:: python 10 | 11 | import argparse 12 | 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("name", nargs='?', 15 | help="The person to greet") 16 | parser.add_argument("repeat", nargs='?', type=int, default=1, 17 | help="How many times should the person be greeted") 18 | parser.add_argument("--no-capitalize", action='store_const', const=True) 19 | 20 | args = parser.parse_args() 21 | 22 | if args.name is None: 23 | greeting = 'Hello world!' 24 | else: 25 | name = args.name if args.no_capitalize else args.name.title() 26 | greeting = 'Hello {0}!'.format(name) 27 | 28 | for _ in range(args.repeat): 29 | print(greeting) 30 | 31 | This doesn't really feel like what Python should be about. It's too much for 32 | such a simple CLI. 33 | 34 | A much simpler alternative could be to use argument unpacking: 35 | 36 | .. code-block:: python 37 | 38 | import sys.argv 39 | 40 | def main(script, name=None, repeat='1'): 41 | if name is None: 42 | greeting = 'Hello world!' 43 | else: 44 | greeting = 'Hello {0}!'.format(name.title()) 45 | 46 | for _ in range(int(repeat)): 47 | print(greeting) 48 | 49 | main(*sys.argv) 50 | 51 | This makes better use of Python concepts: a function bundles a series of 52 | statements with a list of parameters, and that bundle is now accessible from 53 | the command-line. 54 | 55 | However, we lose several features in this process: Our simpler version can't 56 | process named arguments like ``--no-capitalize``, there is no ``--help`` 57 | function of any sort, and all errors just come up as tracebacks, which would be 58 | confusing for the uninitiated. 59 | 60 | Those shortcomings are not inherent to bundling behavior and parameters into 61 | functions. Functions can have keyword-only parameters (and this can be 62 | backported to Python 2.x), and those parameter lists can be examined at run 63 | time. Specific errors can be reformatted, and so forth. 64 | 65 | Clize was made to address these shortcomings while expanding on this idea that 66 | command-line parameters are analogous to those of a function call in Python. 67 | 68 | The following table summarizes a few direct translations Clize makes: 69 | 70 | =================================== =========================================== 71 | Python construct Command-line equivalent 72 | =================================== =========================================== 73 | Function Command 74 | List of functions Multiple commands 75 | Docstring Source for the ``--help`` output 76 | Decorator Transformation of a command 77 | Positional parameter Positional parameter 78 | Keyword-only parameter Named parameter (like ``--one``) 79 | Parameter with a default value Optional parameter 80 | Parameter with `False` as default Flag (`True` if present, `False` otherwise) 81 | =================================== =========================================== 82 | 83 | Some concepts fall outside these straightforward relationships, but in all 84 | cases your part of the command-line interface remains a normal function. You 85 | can call that function normally, have another command from your CLI use it, or 86 | test it like any other function. 87 | 88 | For when Python constructs aren't enough, Clize uses parameter annotations, a 89 | yet mostly unexplored feature of Python 3. For instance, you can specify value 90 | converters for the received arguments, or replace how a parameter is 91 | implemented completely. 92 | 93 | Even though its basic purpose could be called *magicky*, Clize attempts to 94 | limit magic in the sense that anything Clize's own parameters do can also be 95 | done in custom parameters. For instance, ``--help`` in a command-line will 96 | trigger the displaying of the help screen, even if there were errors 97 | beforehand. You might never need to do this, but the option is there if and 98 | when you ever need this. `argparse` for instance does not let you do this. 99 | 100 | With Clize, you start simple but you remain flexible throughout, with options for refactoring and extending your command-line interface. 101 | 102 | 103 | .. _other parsers: 104 | 105 | "Why not use an existing parser instead of making your own?" 106 | ------------------------------------------------------------ 107 | 108 | Argument parsing is a rather common need for Python programs. As such, there 109 | are many argument parsers in existence, including no less than three in the 110 | standard library alone! 111 | 112 | The general answer is that they are different. The fact that there are so many 113 | different parsers available shows that argument parsing APIs are far from being 114 | a "solved problem". Clize offers a very different approach from those of 115 | `argparse`, `Click `_ or `docopt 116 | `_. Each of these always have you write a specification for 117 | your CLI. 118 | 119 | Clize comes with less batteries included than the above. It focuses on 120 | providing just the behavior that corresponds with Python parameters, along with 121 | just a few extras. Clize can afford to do this because unlike these, Clize can 122 | be extended to accommodate custom needs. Every parameter kind can be implemented 123 | by external code and made usable the same way as `clize.parameters.multi` or 124 | `clize.parameters.argument_decorator`. 125 | 126 | 127 | .. _wrapper around argparse: 128 | 129 | "Why not create a thin wrapper around argparse?" 130 | ------------------------------------------------ 131 | 132 | Back during Clize's first release, `argparse`'s parser would have been 133 | sufficient for what Clize proposed, though I wasn't really interested in 134 | dealing with it at the time. With Clize 3's extensible parser, replacing it 135 | with `argparse` would be a loss in flexibility, in parameter capabilities and 136 | help message formatting. 137 | -------------------------------------------------------------------------------- /examples/README.rst: -------------------------------------------------------------------------------- 1 | .. |docs| replace:: http://clize.readthedocs.io/en/latest/ 2 | 3 | Examples 4 | ======== 5 | 6 | ``helloworld.py`` 7 | The most basic use of Clize, a function passed to ``run`` which just 8 | prints "Hello world!" by returning it. 9 | 10 | ``hello.py`` 11 | Greets a person or the world. Demonstrates the specification of positional 12 | and named optional parameters. 13 | 14 | ``echo.py`` 15 | Prints back some text. Demonstrates the use of ``*args``-like parameters to 16 | collect all arguments, flags, short aliases for named parameters, using 17 | ``ArgumentError`` for arbitrary requirements, as well as supplying an 18 | alternate action to ``run``. 19 | 20 | ``altcommands.py`` 21 | Demonstrates the specification of alternate actions. 22 | 23 | ``multicommands.py`` 24 | Demonstrates the specification of multiple commands. 25 | 26 | ``deco_add_param.py`` 27 | Uses a decorator to add a parameter to a function and change its return 28 | value. 29 | 30 | ``deco_provide_arg.py`` 31 | Uses a decorator to supply an argument to a function depending on several 32 | parameters. 33 | 34 | ``argdeco.py`` 35 | Uses a decorator to add parameters that qualify another parameter. 36 | 37 | ``multi.py`` 38 | Uses ``clize.parameters.multi`` to let a named parameter be specified 39 | multiple times. 40 | 41 | ``mapped.py`` 42 | Demonstrates the use of ``clize.parameters.mapped``, limiting the values 43 | accepted by the parameter. 44 | 45 | ``bfparam.py`` 46 | Reimplements a minimal version of ``clize.parameters.one_of``. Demonstrates 47 | subclassing a parameter, replacing its value processing, adding info to the 48 | help page and creating a parameter converter for it ``mapped.py``. 49 | 50 | ``logparam.py`` 51 | Extends ``FlagParameter`` with an alternate value converter, fixing the 52 | default value display. 53 | 54 | ``typed_cli.py`` 55 | Demonstrates concurrent usage of Clize and mypy. 56 | 57 | ``naval.py`` 58 | Clize version of `docopt`_'s "naval fate" example. 59 | 60 | ``interop.py`` 61 | Demonstrates using a different argument parser (here, `argparse`_) 62 | among other commands that use Clize. 63 | 64 | .. _argparse: https://docs.python.org/3/library/argparse.html 65 | .. _docopt: http://docopt.org/ -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsy/clize/00af7c048f6cb78e0eda2236e58d8c238a01102e/examples/__init__.py -------------------------------------------------------------------------------- /examples/altcommands.py: -------------------------------------------------------------------------------- 1 | from clize import run 2 | 3 | 4 | VERSION = "0.2" 5 | 6 | 7 | def do_nothing(): 8 | """Does nothing""" 9 | return "I did nothing, I swear!" 10 | 11 | 12 | def version(): 13 | """Show the version""" 14 | return 'Do Nothing version {0}'.format(VERSION) 15 | 16 | 17 | run(do_nothing, alt=version) 18 | -------------------------------------------------------------------------------- /examples/argdeco.py: -------------------------------------------------------------------------------- 1 | from clize import run 2 | from clize.parameters import argument_decorator 3 | 4 | 5 | @argument_decorator 6 | def capitalize(arg, *, capitalize:('c', 'upper')=False, reverse:'r'=False): 7 | """ 8 | Options to qualify {param}: 9 | 10 | :param capitalize: Make {param} uppercased 11 | :param reverse: Reverse {param} 12 | """ 13 | if capitalize: 14 | arg = arg.upper() 15 | if reverse: 16 | arg = arg[::-1] 17 | return arg 18 | 19 | 20 | def main(*args:capitalize): 21 | """ 22 | :param args: Words to print 23 | """ 24 | return ' '.join(args) 25 | 26 | 27 | run(main) 28 | -------------------------------------------------------------------------------- /examples/bfparam.py: -------------------------------------------------------------------------------- 1 | from clize import run, parser, errors 2 | 3 | 4 | class _ShowList(Exception): 5 | pass 6 | 7 | 8 | class OneOfParameter(parser.ParameterWithValue): 9 | def __init__(self, values, **kwargs): 10 | super().__init__(**kwargs) 11 | self.values = values 12 | 13 | def coerce_value(self, arg, ba): 14 | if arg == 'list': 15 | raise _ShowList 16 | elif arg in self.values: 17 | return arg 18 | else: 19 | raise errors.BadArgumentFormat(arg) 20 | 21 | def read_argument(self, ba, i): 22 | try: 23 | super(OneOfParameter, self).read_argument(ba, i) 24 | except _ShowList: 25 | ba.func = self.show_list 26 | ba.args[:] = [] 27 | ba.kwargs.clear() 28 | ba.sticky = parser.IgnoreAllArguments() 29 | ba.posarg_only = True 30 | 31 | def show_list(self): 32 | for val in self.values: 33 | print(val) 34 | 35 | def help_parens(self): 36 | for s in super(OneOfParameter, self).help_parens(): 37 | yield s 38 | yield 'use "list" for options' 39 | 40 | 41 | def one_of(*values): 42 | return parser.use_mixin(OneOfParameter, kwargs={'values': values}) 43 | 44 | 45 | def func(breakfast:one_of('ham', 'spam')): 46 | """Serves breakfast 47 | 48 | :param breakfast: what food to serve 49 | """ 50 | print("{0} is served!".format(breakfast)) 51 | 52 | 53 | run(func) 54 | -------------------------------------------------------------------------------- /examples/deco_add_param.py: -------------------------------------------------------------------------------- 1 | from sigtools.wrappers import decorator 2 | from clize import run 3 | 4 | 5 | @decorator 6 | def with_uppercase(wrapped, *args, uppercase=False, **kwargs): 7 | """ 8 | Formatting options: 9 | 10 | :param uppercase: Print output in capitals 11 | """ 12 | ret = wrapped(*args, **kwargs) 13 | if uppercase: 14 | return str(ret).upper() 15 | else: 16 | return ret 17 | 18 | 19 | @with_uppercase 20 | def hello_world(name=None): 21 | """Says hello world 22 | 23 | :param name: Who to say hello to 24 | """ 25 | if name is not None: 26 | return 'Hello ' + name 27 | else: 28 | return 'Hello world!' 29 | 30 | 31 | if __name__ == '__main__': 32 | run(hello_world) 33 | -------------------------------------------------------------------------------- /examples/deco_provide_arg.py: -------------------------------------------------------------------------------- 1 | from sigtools.wrappers import decorator 2 | from clize import run 3 | 4 | 5 | def get_branch_object(repository, branch_name): 6 | return repository, branch_name 7 | 8 | 9 | @decorator 10 | def with_branch(wrapped, *args, repository='.', branch='master', **kwargs): 11 | """Decorate with this so your function receives a branch object 12 | 13 | :param repository: A directory belonging to the repository to operate on 14 | :param branch: The name of the branch to operate on 15 | """ 16 | return wrapped(*args, branch=get_branch_object(repository, branch), **kwargs) 17 | 18 | 19 | @with_branch 20 | def diff(*, branch=None): 21 | """Show the differences between the committed code and the working tree.""" 22 | return "I'm different." 23 | 24 | 25 | @with_branch 26 | def commit(*text, branch=None): 27 | """Commit the changes. 28 | 29 | :param text: A message to store alongside the commit 30 | """ 31 | return "All saved.: " + ' '.join(text) 32 | 33 | 34 | @with_branch 35 | def revert(*, branch=None): 36 | """Revert the changes made in the working tree.""" 37 | return "All changes reverted!" 38 | 39 | 40 | run(diff, commit, revert, 41 | description="A mockup version control system(like git, hg or bzr)") 42 | -------------------------------------------------------------------------------- /examples/echo.py: -------------------------------------------------------------------------------- 1 | from clize import ArgumentError, Parameter, run 2 | 3 | def echo(*text:Parameter.REQUIRED, 4 | prefix:'p'='', suffix:'s'='', reverse:'r'=False, repeat:'n'=1): 5 | """Echoes text back 6 | 7 | :param text: The text to echo back 8 | :param reverse: Reverse text before processing 9 | :param repeat: Amount of times to repeat text 10 | :param prefix: Prepend this to each line in word 11 | :param suffix: Append this to each line in word 12 | """ 13 | text = ' '.join(text) 14 | if 'spam' in text: 15 | raise ArgumentError("I don't want any spam!") 16 | if reverse: 17 | text = text[::-1] 18 | text = text * repeat 19 | if prefix or suffix: 20 | return '\n'.join(prefix + line + suffix 21 | for line in text.split('\n')) 22 | return text 23 | 24 | def version(): 25 | """Show the version""" 26 | return 'echo version 0.2' 27 | 28 | if __name__ == '__main__': 29 | run(echo, alt=version) 30 | -------------------------------------------------------------------------------- /examples/hello.py: -------------------------------------------------------------------------------- 1 | from clize import run 2 | 3 | def hello_world(name=None, *, no_capitalize=False): 4 | """Greets the world or the given name. 5 | 6 | :param name: If specified, only greet this person. 7 | :param no_capitalize: Don't capitalize the given name. 8 | """ 9 | if name: 10 | if not no_capitalize: 11 | name = name.title() 12 | return 'Hello {0}!'.format(name) 13 | return 'Hello world!' 14 | 15 | if __name__ == '__main__': 16 | run(hello_world) 17 | -------------------------------------------------------------------------------- /examples/helloworld.py: -------------------------------------------------------------------------------- 1 | from clize import run 2 | 3 | def hello_world(): 4 | return "Hello world!" 5 | 6 | if __name__ == '__main__': 7 | run(hello_world) 8 | -------------------------------------------------------------------------------- /examples/interop.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from clize import Clize, parameters, run 4 | 5 | 6 | @Clize.as_is(description="Prints argv separated by pipes") 7 | def echo_argv(*args): 8 | print(*args, sep=' | ') 9 | 10 | 11 | def using_argparse(name: parameters.pass_name, *args): 12 | parser = argparse.ArgumentParser(prog=name) 13 | parser.add_argument('--ham') 14 | ns = parser.parse_args(args=args) 15 | print(ns.ham) 16 | 17 | 18 | run(echo_argv, 19 | Clize.as_is(using_argparse, 20 | description="Prints the value of the --ham option", 21 | usages=['--help', '[--ham HAM]'])) 22 | -------------------------------------------------------------------------------- /examples/logparam.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sigtools import wrappers 4 | from clize import run, parser, util 5 | 6 | 7 | levels = { 8 | 'CRITICAL': logging.CRITICAL, 9 | 'ERROR': logging.ERROR, 10 | 'WARNING': logging.WARNING, 11 | 'INFO': logging.INFO, 12 | 'DEBUG': logging.DEBUG, 13 | 'NOTSET': logging.NOTSET 14 | } 15 | 16 | 17 | @parser.value_converter 18 | def loglevel(arg): 19 | try: 20 | return int(arg) 21 | except ValueError: 22 | try: 23 | return levels[arg.upper()] 24 | except KeyError: 25 | raise ValueError(arg) 26 | 27 | 28 | class LogLevelParameter(parser.FlagParameter): 29 | def __init__(self, conv, value=logging.INFO, **kwargs): 30 | super(LogLevelParameter, self).__init__( 31 | conv=loglevel, value=value, **kwargs) 32 | 33 | def help_parens(self): 34 | if self.default is not util.UNSET: 35 | for k, v in levels.items(): 36 | if v == self.default: 37 | default = k 38 | break 39 | else: 40 | default = self.default 41 | yield 'default: {0}'.format(default) 42 | 43 | 44 | log_level = parser.use_class(named=LogLevelParameter) 45 | 46 | 47 | def try_log(logger): 48 | logger.debug("Debug") 49 | logger.info("Info") 50 | logger.warning("Warning") 51 | logger.error("Error") 52 | logger.critical("Critical") 53 | 54 | 55 | @wrappers.decorator 56 | def with_logger(wrapped, *args, log:log_level=logging.CRITICAL, **kwargs): 57 | """ 58 | Logging options: 59 | 60 | :param log: The desired log level""" 61 | logger = logging.getLogger('myapp') 62 | logger.setLevel(log) 63 | logger.addHandler(logging.StreamHandler()) 64 | return wrapped(*args, logger=logger, **kwargs) 65 | 66 | 67 | @with_logger 68 | def main(*, logger): 69 | """Tries out the logging system""" 70 | try_log(logger) 71 | 72 | 73 | run(main) 74 | -------------------------------------------------------------------------------- /examples/mapped.py: -------------------------------------------------------------------------------- 1 | from clize import run, parameters 2 | 3 | 4 | greeting = parameters.mapped([ 5 | ('Hello', ['hello', 'hi'], 'A welcoming message'), 6 | ('Goodbye', ['goodbye', 'bye'], 'A parting message'), 7 | ]) 8 | 9 | 10 | def main(name='world', *, kind:('k', greeting)='Hello'): 11 | """ 12 | :param name: Who is the message for? 13 | :param kind: What kind of message should be given to name? 14 | """ 15 | return '{0} {1}!'.format(kind, name) 16 | 17 | 18 | run(main) 19 | -------------------------------------------------------------------------------- /examples/multi.py: -------------------------------------------------------------------------------- 1 | from clize import run, parameters 2 | 3 | 4 | def main(*, listen:('l', parameters.multi(min=1, max=3))): 5 | """Listens on the given addresses 6 | 7 | :param listen: An address to listen on. 8 | """ 9 | for address in listen: 10 | print('Listening on {0}'.format(address)) 11 | 12 | 13 | run(main) 14 | -------------------------------------------------------------------------------- /examples/multicommands.py: -------------------------------------------------------------------------------- 1 | from clize import run 2 | 3 | 4 | def add(*text): 5 | """Adds an entry to the to-do list. 6 | 7 | :param text: The text associated with the entry. 8 | """ 9 | return "OK I will remember that." 10 | 11 | 12 | def list_(): 13 | """Lists the existing entries.""" 14 | return "Sorry I forgot it all :(" 15 | 16 | 17 | run(add, list_, description=""" 18 | A reliable to-do list utility. 19 | 20 | Store entries at your own risk. 21 | """) 22 | -------------------------------------------------------------------------------- /examples/naval.py: -------------------------------------------------------------------------------- 1 | """ 2 | Naval battle example from docopt 3 | """ 4 | 5 | 6 | from collections import OrderedDict 7 | from clize import run, parser 8 | 9 | 10 | def ship_new(name): 11 | """Create a new ship 12 | 13 | name: The name to attribute to the ship 14 | """ 15 | return "Created ship {0}".format(name) 16 | 17 | 18 | knots = parser.value_converter(float, name='KN') 19 | 20 | 21 | def ship_move(ship, x:float, y:float, *, speed:knots=10): 22 | """Move a ship 23 | 24 | ship: The ship which to move 25 | 26 | x: X coordinate 27 | 28 | y: Y coordinate 29 | 30 | speed: Speed in knots 31 | """ 32 | return "Moving ship {0} to {1},{2} with speed {3}".format(ship, x, y, speed) 33 | 34 | 35 | def ship_shoot(ship, x:float, y:float): 36 | """Make a ship fire at the designated coordinates 37 | 38 | ship: The ship which to move 39 | 40 | x: X coordinate 41 | 42 | y: Y coordinate 43 | """ 44 | return "{0} shoots at {1},{2}".format(ship, x, y) 45 | 46 | 47 | def mine_set(x, y, *, drifting=False): 48 | """Set a mine 49 | 50 | x: X coordinate 51 | 52 | y: Y coordinate 53 | 54 | drifting: Don't anchor the mine and let it drift 55 | """ 56 | return "Set {0} mine at {1},{2}".format( 57 | "drifting" if drifting else "anchored", 58 | x, y) 59 | 60 | 61 | def mine_remove(x, y): 62 | """Removes a mine 63 | 64 | x: X coordinate 65 | 66 | y: Y coordinate 67 | """ 68 | return "Removing mine at {0}, {1}".format(x, y) 69 | 70 | 71 | def version(): 72 | return "Version 1.0" 73 | 74 | 75 | run({ 76 | 'ship': OrderedDict([ 77 | ('new', ship_new), 78 | ('move', ship_move), 79 | ('shoot', ship_shoot), 80 | ]), 81 | 'mine': OrderedDict([ 82 | ('set', mine_set), 83 | ('remove', mine_remove), 84 | ]), 85 | }, alt=version) 86 | -------------------------------------------------------------------------------- /examples/typed_cli.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import typing 3 | 4 | from clize import Clize, run, converters # type: ignore 5 | 6 | 7 | def main( 8 | # The ideal case: Clize and mypy understand the same annotation 9 | same_annotation_for_both_str: str, 10 | same_annotation_for_both_converter: pathlib.Path, 11 | *, 12 | # Unfortunately, Clize doesn't understand typing.Optional yet, and just uses int. 13 | # You'll have to separate the typing and Clize annotation using typing.Annotated 14 | optional_value: typing.Annotated[typing.Optional[int], Clize[int]] = None, 15 | # Perhaps confusingly, typing.Optional does not refer to whether a parameter is required, 16 | # only whether None is an acceptable value. 17 | optional_parameter: typing.Annotated[int, Clize[int]] = 1, 18 | # If you're using other clize annotations, like parameter aliases, 19 | # you'll have to use typing.Annotated 20 | aliased: typing.Annotated[int, Clize["n"]], 21 | # Value converters do not yet create typing annotations, 22 | # so you have to define the type separately using typing.Annotated. 23 | # Additionally, the type created by converters.file() is not public, so you have to rely on Any for now. 24 | file_opener: typing.Annotated[typing.Any, Clize[converters.file()]] 25 | ): 26 | """ 27 | Example CLI that uses typing and Clize together 28 | 29 | In Clize 5.0 this remains fairly rudimentary, 30 | so you may have to repeat yourself 31 | when Clize and your type checker (e.g. mypy) do not understand the same annotation. 32 | """ 33 | print( 34 | same_annotation_for_both_str.join(["abc"]), 35 | same_annotation_for_both_converter.exists(), 36 | optional_value + 1 if optional_value is not None else 0, 37 | optional_parameter + 1, 38 | aliased + 1, 39 | file_opener, 40 | ) 41 | 42 | 43 | if __name__ == "__main__": 44 | run(main) 45 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | 7 | [[tool.mypy.overrides]] 8 | module = "clize.*" 9 | ignore_errors = true 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [upload_docs] 5 | upload-dir = build/sphinx/html 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | 6 | with open("README.rst") as fh: 7 | long_description = fh.read() 8 | 9 | test_requirements = [ 10 | 'repeated_test>=2.3.0a4', 11 | 'python-dateutil', 12 | 'Pygments', 13 | ] 14 | 15 | setup( 16 | name='clize', 17 | description='Turn functions into command-line interfaces', 18 | long_description=long_description, 19 | long_description_content_type='text/x-rst', 20 | license='MIT', 21 | url='https://github.com/epsy/clize', 22 | author='Yann Kaiser', 23 | author_email='kaiser.yann@gmail.com', 24 | python_requires='>=3.6', 25 | install_requires=[ 26 | 'sigtools >= 4.0.1', 27 | 'attrs>=19.1.0', 28 | 'od', 29 | 'docutils >= 0.17.0', 30 | ], 31 | tests_require=test_requirements, 32 | extras_require={ 33 | 'datetime': ['python-dateutil'], 34 | 'test': test_requirements, 35 | 'clize-own-docs': [ 36 | 'sphinx~=4.2.0', 37 | 'sphinx_rtd_theme', 38 | ], 39 | }, 40 | packages=('clize', 'clize.tests'), 41 | test_suite='clize.tests', 42 | keywords=[ 43 | 'CLI', 'options', 'arguments', 'getopts', 'getopt', 'argparse', 44 | 'introspection', 'flags', 'decorator', 'subcommands', 45 | ], 46 | classifiers=[ 47 | "Development Status :: 5 - Production/Stable", 48 | "License :: OSI Approved :: MIT License", 49 | "Programming Language :: Python :: 3", 50 | "Programming Language :: Python :: 3.6", 51 | "Programming Language :: Python :: 3.7", 52 | "Programming Language :: Python :: 3.8", 53 | "Programming Language :: Python :: 3.9", 54 | "Programming Language :: Python :: 3.10", 55 | "Programming Language :: Python :: Implementation :: CPython", 56 | "Programming Language :: Python :: Implementation :: PyPy", 57 | "Environment :: Console", 58 | "Intended Audience :: Developers", 59 | "Intended Audience :: System Administrators", 60 | "Operating System :: OS Independent", 61 | "Topic :: Software Development", 62 | "Topic :: Software Development :: Libraries :: Python Modules", 63 | "Topic :: Software Development :: User Interfaces", 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=test-{py39,py310},cover-py310-all,pyflakes,typecheck-py310-example 3 | 4 | [testenv] 5 | skip_install=true 6 | deps= 7 | -e .[test] 8 | docs: -e .[clize-own-docs] 9 | cover: coverage 10 | typecheck: mypy 11 | 12 | commands= 13 | test: {posargs:python -m unittest} 14 | cover: coverage erase 15 | cover-all: -coverage run {posargs:-m unittest} 16 | cover-util: -coverage run -m unittest2 clize.tests.test_util 17 | cover-help: -coverage run -m unittest2 clize.tests.test_help 18 | cover: coverage html 19 | cover: coverage xml 20 | cover-all: coverage report 21 | cover-util: coverage report --include 'clize/util.py' 22 | cover-help: coverage report --include 'clize/help.py' 23 | docs: {envbindir}/sphinx-build {toxinidir}/docs/ {toxinidir}/build/sphinx {posargs:} 24 | typecheck-example: python -m mypy --install-types --non-interactive --ignore-missing-imports examples/typed_cli.py 25 | 26 | [testenv:docs] 27 | basepython=python3.9 28 | 29 | [testenv:pyflakes] 30 | deps= 31 | pyflakes 32 | commands= 33 | pyflakes clize 34 | --------------------------------------------------------------------------------