├── mando ├── tests │ ├── __init__.py │ ├── run.py │ ├── test_unicode_docstring_on_py2.py │ ├── capture.py │ ├── test_google.py │ ├── test_numpy.py │ ├── test_utils.py │ └── test_core.py ├── __init__.py ├── napoleon │ ├── pycompat.py │ ├── iterators.py │ ├── __init__.py │ └── docstring.py ├── rst_text_formatter.py ├── utils.py └── core.py ├── setup.cfg ├── test_requirements.pip ├── .landscape.yml ├── MANIFEST.in ├── .readthedocs.yaml ├── Pipfile ├── examples ├── docs │ ├── default_args.py │ ├── short_options.py │ ├── command.py │ └── types.py ├── pow_arg.py ├── echo.py ├── pow.py ├── git.py └── gnu.py ├── tox.ini ├── .travis.yml ├── .gitignore ├── Makefile ├── CHANGELOG ├── LICENSE ├── setup.py ├── docs ├── index.rst ├── Makefile ├── make.bat ├── conf.py └── usage.rst ├── README.rst └── pylintrc /mando/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /test_requirements.pip: -------------------------------------------------------------------------------- 1 | python-coveralls 2 | pytest 3 | coverage 4 | tox 5 | -------------------------------------------------------------------------------- /.landscape.yml: -------------------------------------------------------------------------------- 1 | strictness: high 2 | ignore_paths: 3 | - docs 4 | - examples 5 | - setup.py 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | recursive-include docs * 4 | recursive-include examples *.py 5 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3.10" 12 | -------------------------------------------------------------------------------- /examples/docs/default_args.py: -------------------------------------------------------------------------------- 1 | from mando import command, main 2 | 3 | 4 | @command 5 | def po(a=2, b=3): 6 | print(a ** b) 7 | 8 | 9 | if __name__ == '__main__': 10 | main() 11 | -------------------------------------------------------------------------------- /mando/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.8.2' 2 | 3 | from mando.core import Program 4 | 5 | main = Program() 6 | command = main.command 7 | arg = main.arg 8 | parse = main.parse 9 | execute = main.execute 10 | -------------------------------------------------------------------------------- /mando/napoleon/pycompat.py: -------------------------------------------------------------------------------- 1 | class UnicodeMixin: 2 | """Mixin class to handle defining the proper __str__/__unicode__ 3 | methods in Python 2 or 3.""" 4 | 5 | def __str__(self): 6 | return self.__unicode__() 7 | -------------------------------------------------------------------------------- /examples/pow_arg.py: -------------------------------------------------------------------------------- 1 | from mando import main, arg, command 2 | 3 | 4 | @command 5 | @arg('base', type=int) 6 | @arg('exp', type=int) 7 | def pow(base, exp): 8 | print base ** exp 9 | 10 | 11 | if __name__ == '__main__': 12 | main() 13 | -------------------------------------------------------------------------------- /mando/tests/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | if __name__ == '__main__': 4 | import sys 5 | import pytest 6 | 7 | if sys.version_info[:2] >= (3, 10): 8 | pytest.main(['--strict-markers']) 9 | else: 10 | pytest.main(['--strict']) 11 | -------------------------------------------------------------------------------- /examples/echo.py: -------------------------------------------------------------------------------- 1 | from mando import command, main 2 | 3 | @command 4 | def echo(text, capitalize=False): 5 | '''Echo the given text.''' 6 | if capitalize: 7 | text = text.upper() 8 | print text 9 | 10 | 11 | if __name__ == '__main__': 12 | main() 13 | -------------------------------------------------------------------------------- /examples/pow.py: -------------------------------------------------------------------------------- 1 | from mando import main, command 2 | 3 | 4 | @command 5 | def pow(base, exp): 6 | '''Compute base ^ exp. 7 | 8 | :param base : The base. 9 | :param exp : The exponent.''' 10 | print base ** exp 11 | 12 | 13 | if __name__ == '__main__': 14 | main() 15 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35,py36,py37,py38,pypy,pypy3 3 | 4 | [testenv] 5 | deps = pytest 6 | commands = python mando/tests/run.py 7 | downloadcache = build 8 | 9 | [testenv:py26] 10 | deps = 11 | argparse 12 | pytest 13 | commands = python mando/tests/run.py 14 | downloadcache = build 15 | -------------------------------------------------------------------------------- /examples/docs/short_options.py: -------------------------------------------------------------------------------- 1 | from mando import command, main 2 | 3 | 4 | @command 5 | def ex(foo, b=None, spam=None): 6 | '''Nothing interesting. 7 | 8 | :param foo: Bla bla. 9 | :param -b: A little flag. 10 | :param -s, --spam: Spam spam spam spam.''' 11 | 12 | print(foo, b, spam) 13 | 14 | if __name__ == '__main__': 15 | main() 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | - "3.6" 6 | - "3.7" 7 | - "3.8" 8 | - "pypy" 9 | - "pypy3" 10 | install: 11 | - pip install -U pip 12 | - pip install -e . 13 | - pip install -r test_requirements.pip 14 | script: 15 | - make tests 16 | - make cov 17 | after_success: 18 | - coveralls 19 | -------------------------------------------------------------------------------- /examples/docs/command.py: -------------------------------------------------------------------------------- 1 | from mando import command, main 2 | 3 | 4 | @command 5 | def cmd(foo, bar): 6 | '''Here stands the help. 7 | 8 | And here the description of this useless command. 9 | 10 | :param foo: Well, the first arg. 11 | :param bar: Obviously the second arg. Nonsense.''' 12 | 13 | print(foo, bar) 14 | 15 | 16 | if __name__ == '__main__': 17 | main() 18 | -------------------------------------------------------------------------------- /examples/docs/types.py: -------------------------------------------------------------------------------- 1 | from mando import command, main 2 | 3 | 4 | @command 5 | def pow(a, b, mod=None): 6 | '''Mimic Python's pow() function. 7 | 8 | :param a : The base. 9 | :param b : The exponent. 10 | :param -m, --mod : Modulus.''' 11 | 12 | if mod is not None: 13 | print((a ** b) % mod) 14 | else: 15 | print(a ** b) 16 | 17 | 18 | if __name__ == '__main__': 19 | main() 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.sw[op] 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | _build 13 | eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | __pycache__ 23 | .mypy_cache 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | .coverage 30 | .tox 31 | nosetests.xml 32 | htmlcov 33 | .cache 34 | venv 35 | -------------------------------------------------------------------------------- /mando/tests/test_unicode_docstring_on_py2.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mando import Program 4 | 5 | 6 | program = Program('example.py', '1.0.10') 7 | 8 | 9 | class Test_unicode_docstring_on_py2(unittest.TestCase): 10 | 11 | def test_py2_unicode_literals(self): 12 | @program.command 13 | def some_command(): 14 | 'this is a unicode doc-string!' 15 | 16 | assert not isinstance(some_command.__doc__, bytes) 17 | # TODO: check that the generated help is correct 18 | -------------------------------------------------------------------------------- /mando/tests/capture.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Capture function 5 | ---------------------------------- 6 | 7 | ''' 8 | 9 | import sys 10 | from contextlib import contextmanager 11 | from io import StringIO 12 | 13 | @contextmanager 14 | def capture_sys_output(): 15 | capture_out, capture_err = StringIO(), StringIO() 16 | current_out, current_err = sys.stdout, sys.stderr 17 | try: 18 | sys.stdout, sys.stderr = capture_out, capture_err 19 | yield capture_out, capture_err 20 | finally: 21 | sys.stdout, sys.stderr = current_out, current_err 22 | -------------------------------------------------------------------------------- /examples/git.py: -------------------------------------------------------------------------------- 1 | from mando import command, main 2 | 3 | 4 | @command 5 | def push(repository, all=False, dry_run=False, force=False, thin=False): 6 | '''Update remote refs along with associated objects. 7 | 8 | :param repository: Repository to push to. 9 | :param --all: Push all refs. 10 | :param -n, --dry-run: Dry run. 11 | :param -f, --force: Force updates. 12 | :param --thin: Use thin pack.''' 13 | 14 | print ('Pushing to {0}. All: {1}, dry run: {2}, force: {3}, thin: {4}' 15 | .format(repository, all, dry_run, force, thin)) 16 | 17 | 18 | if __name__ == '__main__': 19 | main() 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tests cov htmlcov pep8 pylint docs dev-deps test-deps publish coveralls 2 | 3 | tests: 4 | python mando/tests/run.py 5 | 6 | cov: 7 | coverage erase && coverage run --include "mando/*" --omit "mando/tests/*,mando/napoleon/*" mando/tests/run.py 8 | coverage report -m 9 | 10 | htmlcov: cov 11 | coverage html 12 | 13 | pep8: 14 | pep8 mando --exclude "tests" 15 | 16 | pylint: 17 | pylint --rcfile pylintrc mando 18 | 19 | docs: 20 | cd docs && make html 21 | 22 | dev-deps: 23 | pip install -r dev_requirements.pip 24 | 25 | test-deps: 26 | pip install -r test_requirements.pip 27 | 28 | publish: 29 | rm -rf dist/* 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | python setup.py develop 33 | 34 | coveralls: test-deps cov 35 | coveralls 36 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.8.2 (Oct 20, 2024) 2 | -------------------- 3 | 4 | - Update support versions trove classifiers (@a-detiste): #58 5 | 6 | 0.8.1 (Oct 20, 2024) 7 | -------------------- 8 | 9 | - Remove funcsigs dependency 10 | 11 | 0.8.0 (Oct 20, 2024) 12 | -------------------- 13 | 14 | - Drop support for Python 2 and the dependency on six (@a-detiste): #57 15 | 16 | 0.7.1 (Feb 24, 2022) 17 | -------------------- 18 | 19 | - Add support for Python 3.10 (@s-t-e-v-e-n-k): #54 20 | - Fix minor documentation issues. 21 | 22 | 0.7.0 (Mar 16, 2020) 23 | -------------------- 24 | 25 | - Switch from inspect.getargspec to inspect.signature (@acetylen): #47 26 | - Add support for type annotations (@acetylen): #47 27 | - Add support for Python 3.7 and 3.8 (@acetylen): #47 28 | - Remove support for Python 2.6 and 3.4 (@acetylen): #47 29 | -------------------------------------------------------------------------------- /examples/gnu.py: -------------------------------------------------------------------------------- 1 | # gnu.py 2 | from mando import main, command, arg 3 | 4 | 5 | @command 6 | @arg('maxdepth', metavar='') 7 | def find(path, pattern, maxdepth=None, P=False, D=None): 8 | '''Mock some features of the GNU find command. 9 | 10 | This is not at all a complete program, but a simple representation to 11 | showcase mando's coolest features. 12 | 13 | :param path: The starting path. 14 | :param pattern: The pattern to look for. 15 | :param -d, --maxdepth : Descend at most . 16 | :param -P: Do not follow symlinks. 17 | :param -D : Debug option, print diagnostic information.''' 18 | 19 | if maxdepth is not None and maxdepth < 2: 20 | print('If you choose maxdepth, at least set it > 1') 21 | if P: 22 | print('Following symlinks...') 23 | print('Debug options: {0}'.format(D)) 24 | print('Starting search with pattern: {0}'.format(pattern)) 25 | print('No file found!') 26 | 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /mando/rst_text_formatter.py: -------------------------------------------------------------------------------- 1 | 2 | import argparse 3 | import sys 4 | 5 | from rst2ansi import rst2ansi 6 | 7 | 8 | def b(s): 9 | # Useful for very coarse version differentiation. 10 | PY2 = sys.version_info[0] == 2 11 | PY3 = sys.version_info[0] == 3 12 | if PY3: 13 | return s.encode("utf-8") 14 | else: 15 | return s 16 | 17 | 18 | class RSTHelpFormatter(argparse.RawTextHelpFormatter): 19 | """ 20 | Custom formatter class that is capable of interpreting ReST. 21 | """ 22 | def format_help(self): 23 | ret = rst2ansi(b(super(RSTHelpFormatter, self).format_help()) + 24 | b('\n')) 25 | return ret.encode(sys.stdout.encoding, 26 | 'replace').decode(sys.stdout.encoding) 27 | 28 | def format_usage(self): 29 | ret = rst2ansi(b(super(RSTHelpFormatter, self).format_usage()) + 30 | b('\n')) 31 | return ret.encode(sys.stdout.encoding, 32 | 'replace').decode(sys.stdout.encoding) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Michele Lacchia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /mando/tests/test_google.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | from mando import Program 4 | 5 | from . import capture 6 | 7 | program = Program('example.py', '1.0.10') 8 | 9 | 10 | @program.command(doctype='google') 11 | def simple_google_docstring(arg1, arg2="string"): 12 | '''One line summary. 13 | 14 | Extended description. 15 | 16 | Args: 17 | arg1(int): Description of `arg1` 18 | arg2(str): Description of `arg2` 19 | Returns: 20 | str: Description of return value. 21 | ''' 22 | return int(arg1) * arg2 23 | 24 | 25 | GENERIC_COMMAND_CASES = [ 26 | ('simple_google_docstring 2 --arg2=test', 'testtest'), 27 | ] 28 | 29 | 30 | @pytest.mark.parametrize('args,result', GENERIC_COMMAND_CASES) 31 | def test_generic_command(args, result): 32 | args = args.split() 33 | assert result == program.execute(args) 34 | assert program.parse(args)[0].__name__ == program._current_command 35 | 36 | ending = 'al arguments' 37 | if sys.version_info[:2] >= (3, 10): 38 | ending = 's' 39 | GOOGLE_DOCSTRING_HELP_CASES = [ 40 | ('simple_google_docstring --help 2 --arg2=test', '''usage: example.py simple_google_docstring [-h] [--arg2 ARG2] arg1 41 | 42 | Extended description. 43 | 44 | positional arguments: 45 | arg1 Description of `arg1` 46 | 47 | option%s: 48 | -h, --help show this help message and exit 49 | --arg2 ARG2 Description of `arg2` 50 | ''' % ending), 51 | ] 52 | 53 | 54 | @pytest.mark.parametrize('args,result', GOOGLE_DOCSTRING_HELP_CASES) 55 | def test_google_docstring_help(args, result): 56 | args = args.split() 57 | with pytest.raises(SystemExit): 58 | with capture.capture_sys_output() as (stdout, stderr): 59 | program.execute(args) 60 | assert result == stdout.getvalue() 61 | -------------------------------------------------------------------------------- /mando/tests/test_numpy.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | from mando import Program 4 | 5 | from . import capture 6 | 7 | program = Program('example.py', '1.0.10') 8 | 9 | 10 | @program.command(doctype='numpy') 11 | def simple_numpy_docstring(arg1, arg2='string'): 12 | '''One line summary. 13 | 14 | Extended description. 15 | 16 | Parameters 17 | ---------- 18 | arg1 : int 19 | Description of `arg1` 20 | arg2 : str 21 | Description of `arg2` 22 | Returns 23 | ------- 24 | str 25 | Description of return value. 26 | ''' 27 | return int(arg1) * arg2 28 | 29 | 30 | GENERIC_COMMAND_CASES = [ 31 | ('simple_numpy_docstring 2 --arg2=test', 'testtest'), 32 | ] 33 | 34 | 35 | @pytest.mark.parametrize('args,result', GENERIC_COMMAND_CASES) 36 | def test_generic_command(args, result): 37 | args = args.split() 38 | assert result == program.execute(args) 39 | assert program.parse(args)[0].__name__ == program._current_command 40 | 41 | ending = 'al arguments' 42 | if sys.version_info[:2] >= (3, 10): 43 | ending = 's' 44 | NUMPY_DOCSTRING_HELP_CASES = [ 45 | ('simple_numpy_docstring --help 2 --arg2=test', '''usage: example.py simple_numpy_docstring [-h] [--arg2 ARG2] arg1 46 | 47 | Extended description. 48 | 49 | positional arguments: 50 | arg1 Description of `arg1` 51 | 52 | option%s: 53 | -h, --help show this help message and exit 54 | --arg2 ARG2 Description of `arg2` 55 | ''' % ending), 56 | ] 57 | 58 | 59 | @pytest.mark.parametrize('args,result', NUMPY_DOCSTRING_HELP_CASES) 60 | def test_numpy_docstring_help(args, result): 61 | args = args.split() 62 | with pytest.raises(SystemExit): 63 | with capture.capture_sys_output() as (stdout, stderr): 64 | program.execute(args) 65 | assert result == stdout.getvalue() 66 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import setuptools 4 | 5 | try: 6 | import mando 7 | except ImportError as e: 8 | version = e.version 9 | else: 10 | version = mando.__version__ 11 | 12 | deps = [] 13 | extras = {"restructuredText": ["rst2ansi"]} 14 | 15 | 16 | with open(os.path.join(os.path.dirname(__file__), "README.rst")) as fobj: 17 | readme = fobj.read() 18 | 19 | setuptools.setup( 20 | name="mando", 21 | version=version, 22 | author="Michele Lacchia", 23 | author_email="michelelacchia@gmail.com", 24 | url="https://mando.readthedocs.org/", 25 | download_url="https://pypi.python.org/mando/", 26 | license="MIT", 27 | description="Create Python CLI apps with little to no effort at all!", 28 | platforms="any", 29 | long_description=readme, 30 | packages=setuptools.find_packages(), 31 | install_requires=deps, 32 | extras_require=extras, 33 | test_suite="mando.tests", 34 | keywords="argparse,argument parser,arguments,cli,command line," 35 | "commands,decorator,dispatch,flags,getopt,options,optparse," 36 | "parser,subcommands", 37 | classifiers=[ 38 | "Development Status :: 5 - Production/Stable", 39 | "Environment :: Console", 40 | "Intended Audience :: Developers", 41 | "License :: OSI Approved :: MIT License", 42 | "Natural Language :: English", 43 | "Operating System :: OS Independent", 44 | "Programming Language :: Python", 45 | "Programming Language :: Python :: 3", 46 | "Programming Language :: Python :: 3.3", 47 | "Programming Language :: Python :: 3.4", 48 | "Programming Language :: Python :: 3.5", 49 | "Programming Language :: Python :: 3.6", 50 | "Programming Language :: Python :: 3.7", 51 | "Programming Language :: Python :: 3.8", 52 | "Programming Language :: Python :: 3.9", 53 | "Programming Language :: Python :: 3.10", 54 | "Programming Language :: Python :: 3.11", 55 | "Programming Language :: Python :: 3.12", 56 | "Programming Language :: Python :: 3.13", 57 | "Programming Language :: Python :: Implementation :: CPython", 58 | "Programming Language :: Python :: Implementation :: PyPy", 59 | "Topic :: Software Development", 60 | "Topic :: Software Development :: Libraries :: Python Modules", 61 | "Topic :: Utilities", 62 | ], 63 | ) 64 | -------------------------------------------------------------------------------- /mando/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from mando.utils import action_by_type, ensure_dashes, find_param_docs, split_doc 3 | 4 | 5 | ACTION_BY_TYPE_CASES = [ 6 | (True, {'action': 'store_false'}), 7 | (False, {'action': 'store_true'}), 8 | ([], {'action': 'append'}), 9 | ([1, False], {'action': 'append'}), 10 | (None, {}), 11 | (1, {'type': type(1)}), 12 | (1.1, {'type': type(1.1)}), 13 | ('1', {'type': type('1')}), 14 | ] 15 | 16 | 17 | @pytest.mark.parametrize('obj,result', ACTION_BY_TYPE_CASES) 18 | def test_action_by_type(obj, result): 19 | assert result == action_by_type(obj) 20 | 21 | 22 | ENSURE_DASHES_CASES = [ 23 | (['m'], ['-m']), 24 | (['m', 'min'], ['-m', '--min']), 25 | (['-m'], ['-m']), 26 | (['-m', 'min'], ['-m', '--min']), 27 | (['m', '--min'], ['-m', '--min']), 28 | (['-m', '--min'], ['-m', '--min']), 29 | (['-m', '--min', 'l', 'less'], ['-m', '--min', '-l', '--less']), 30 | ] 31 | 32 | 33 | @pytest.mark.parametrize('opts,result', ENSURE_DASHES_CASES) 34 | def test_ensure_dashes(opts, result): 35 | assert result == list(ensure_dashes(opts)) 36 | 37 | 38 | SPLIT_DOC_CASES = [ 39 | ('', ['', '']), 40 | ('only help.', ['only help.', 'only help.']), 41 | ('help.\nstill help.', ['help.\nstill help.', 'help.\nstill help.']), 42 | ('help\n\ndesc', ['help', 'desc']), 43 | ('help\n\n\ndesc\n', ['help', 'desc']), 44 | ] 45 | 46 | 47 | @pytest.mark.parametrize('doc,parts', SPLIT_DOC_CASES) 48 | def test_split_doc(doc, parts): 49 | assert parts == split_doc(doc) 50 | 51 | 52 | a_1 = {'a_param': (['a-param'], {'help': 'Short story.'})} 53 | a_1_1 = {'a_param': (['a_param'], {'help': 'Short story.'})} 54 | a_2 = {'j': (['-j'], {'help': 'Woow'})} 55 | a_3 = {'noun': (['-n', '--noun'], {'help': 'cat'})} 56 | a_all = {} 57 | for a in (a_1, a_2, a_3): 58 | a_all.update(a) 59 | 60 | 61 | FIND_PARAM_CASES = [ 62 | ('', {}), 63 | ('Brevity is the soul of wit.', {}), 64 | (':param a-param: Short story.', a_1), 65 | (':param a_param: Short story.', a_1_1), 66 | (':param -j: Woow', a_2), 67 | (':param -n, --noun: cat', a_3), 68 | (''' 69 | Some short text here and there. 70 | 71 | :param well: water''', {'well': (['well'], {'help': 'water'})}), 72 | (''' 73 | :param a-param: Short story. 74 | :param -j: Woow 75 | :param -n, --noun: cat''', a_all), 76 | (''' 77 | Lemme see. 78 | 79 | :param long-story: A long story believe me: when all started, Adam and Bob were just two little farmers. 80 | ''', {'long_story': (['long-story'], {'help': 'A long story ' 81 | 'believe me: when all started, Adam and ' 82 | 'Bob were just two little farmers.'})}), 83 | ] 84 | 85 | 86 | @pytest.mark.parametrize('doc,params', FIND_PARAM_CASES) 87 | def test_find_param(doc, params): 88 | found_params = find_param_docs(doc) 89 | assert params.keys() == found_params.keys() 90 | for key, value in params.items(): 91 | assert key in found_params 92 | found_value = found_params[key] 93 | assert value[0] == found_value[0] 94 | for kwarg, val in value[1].items(): 95 | assert val == found_value[1][kwarg] 96 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. mando documentation master file, created by 2 | sphinx-quickstart on Wed Dec 4 15:37:28 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | mando - CLI interfaces for Humans 7 | ================================= 8 | 9 | mando is a wrapper around ``argparse``, allowing you to write complete CLI 10 | applications in seconds while maintaining all the flexibility. 11 | 12 | The problem 13 | ----------- 14 | 15 | ``argparse`` is great for single-command applications, which only have some 16 | options and one, default command. Unfortunately, when more commands are added, 17 | the code grows too much along with its complexity. 18 | 19 | The solution 20 | ------------ 21 | mando makes an attempt to simplify this. Since commands are nothing but 22 | functions, mando simply provides a couple of decorators and the job is done. 23 | mando tries to infer as much as possible, in order to allow you to write just 24 | the code that is strictly necessary. 25 | 26 | This example should showcase most of mando's features:: 27 | 28 | # gnu.py 29 | from mando import main, command, arg 30 | 31 | 32 | @arg('maxdepth', metavar='') 33 | def find(path, pattern, maxdepth: int = None, P=False, D=None): 34 | '''Mock some features of the GNU find command. 35 | 36 | This is not at all a complete program, but a simple representation to 37 | showcase mando's coolest features. 38 | 39 | :param path: The starting path. 40 | :param pattern: The pattern to look for. 41 | :param -d, --maxdepth: Descend at most . 42 | :param -P: Do not follow symlinks. 43 | :param -D : Debug option, print diagnostic information.''' 44 | 45 | if maxdepth is not None and maxdepth < 2: 46 | print('If you choose maxdepth, at least set it > 1') 47 | if P: 48 | print('Following symlinks...') 49 | print('Debug options: {0}'.format(D)) 50 | print('Starting search with pattern: {0}'.format(pattern)) 51 | print('No file found!') 52 | 53 | 54 | if __name__ == '__main__': 55 | main() 56 | 57 | mando extracts information from your command's signature and docstring, so you 58 | can document your code and create the CLI application at once! In the above 59 | example the Sphinx format is used, but mando does not force you to write 60 | ReST docstrings. Currently, it supports the following styles: 61 | 62 | - Sphinx (the default one) 63 | - Google 64 | - Numpy 65 | 66 | To see how to specify the docstring format, see :ref:`docstring-style`. 67 | 68 | The first paragraph is taken 69 | to generate the command's *help*. The remaining part (after removing all 70 | ``:param:``'s) is the *description*. For everything that does not fit in the 71 | docstring, mando provides the ``@arg`` decorator, to override arbitrary 72 | arguments before they get passed to ``argparse``. 73 | 74 | .. code-block:: console 75 | 76 | $ python gnu.py -h 77 | usage: gnu.py [-h] {find} ... 78 | 79 | positional arguments: 80 | {find} 81 | find Mock some features of the GNU find command. 82 | 83 | optional arguments: 84 | -h, --help show this help message and exit 85 | 86 | $ python gnu.py find -h 87 | usage: gnu.py find [-h] [-d ] [-P] [-D ] path pattern 88 | 89 | This is not at all a complete program, but a simple representation to showcase 90 | mando's coolest features. 91 | 92 | positional arguments: 93 | path The starting path. 94 | pattern The pattern to look for. 95 | 96 | optional arguments: 97 | -h, --help show this help message and exit 98 | -d , --maxdepth 99 | Descend at most . 100 | -P Do not follow symlinks. 101 | -D Debug option, print diagnostic information. 102 | 103 | As you can see the short options and metavars have been passed to argparse. Now 104 | let's check the program itself: 105 | 106 | .. code-block:: console 107 | 108 | $ python gnu.py find . "*.py" 109 | Debug options: None 110 | Starting search with pattern: *.py 111 | No file found! 112 | $ python gnu.py find . "*.py" -P 113 | Following symlinks... 114 | Debug options: None 115 | Starting search with pattern: *.py 116 | No file found! 117 | $ python gnu.py find . "*" -P -D dbg 118 | Following symlinks... 119 | Debug options: dbg 120 | Starting search with pattern: * 121 | No file found! 122 | $ python gnu.py find . "*" -P -D "dbg,follow,trace" 123 | Following symlinks... 124 | Debug options: dbg,follow,trace 125 | Starting search with pattern: * 126 | No file found! 127 | 128 | $ python gnu.py find -d 1 . "*.pyc" 129 | If you choose maxdepth, at least set it > 1 130 | Debug options: None 131 | Starting search with pattern: *.pyc 132 | No file found! 133 | $ python gnu.py find --maxdepth 0 . "*.pyc" 134 | If you choose maxdepth, at least set it > 1 135 | Debug options: None 136 | Starting search with pattern: *.pyc 137 | No file found! 138 | $ python gnu.py find --maxdepth 4 . "*.pyc" 139 | Debug options: None 140 | Starting search with pattern: *.pyc 141 | No file found! 142 | 143 | $ python gnu.py find --maxdepth 4 . 144 | usage: gnu.py find [-h] [-d ] [-P] [-D ] path pattern 145 | gnu.py find: error: too few arguments 146 | $ python gnu.py find -d "four" . filename 147 | usage: gnu.py find [-h] [-d ] [-P] [-D ] path pattern 148 | gnu.py find: error: argument maxlevels: invalid int value: 'four' 149 | 150 | 151 | 152 | Contents 153 | -------- 154 | 155 | .. toctree:: 156 | :maxdepth: 2 157 | 158 | usage.rst 159 | 160 | 161 | Indices and tables 162 | ================== 163 | 164 | * :ref:`genindex` 165 | * :ref:`modindex` 166 | * :ref:`search` 167 | -------------------------------------------------------------------------------- /mando/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import textwrap 3 | 4 | SPHINX_RE = re.compile( 5 | r'^([\t ]*):' 6 | r'(?Pparam|type|returns|rtype|parameter|arg|argument|key|keyword)' 7 | r' ?(?P[-\w_]+,?)?' 8 | r' ?(?P[-<>\w_]+)?' 9 | r' ?(?P[<>\w_]+)?:' 10 | r'(?P[^\n]*\n+((\1[ \t]+[^\n]*\n)|\n)*)', 11 | re.MULTILINE) 12 | ARG_RE = re.compile( 13 | r'-(?P-)?' 14 | r'(?P(?(long)[^ =,]+|.))[ =]?' 15 | r'(?P[^ ,]+)?') 16 | POS_RE = re.compile( 17 | r'(?P[^ ,]+)?') 18 | ARG_TYPE_MAP = { 19 | 'n': int, 'num': int, 'number': int, 20 | 'i': int, 'int': int, 'integer': int, 21 | 's': str, 'str': str, 'string': str, 22 | 'f': float, 'float': float, 23 | None: None, '': None, 24 | } 25 | 26 | 27 | def purify_doc(string): 28 | '''Remove Sphinx's :param: and :type: lines from the docstring.''' 29 | return SPHINX_RE.sub('', string).rstrip() 30 | 31 | 32 | def split_doc(string): 33 | '''Split the documentation into help and description. 34 | 35 | A two-value list is returned, of the form ``[help, desc]``. If no 36 | description is provided, the help is duplicated.''' 37 | parts = [part.strip() for part in string.split('\n\n', 1)] 38 | if len(parts) == 1: 39 | return parts * 2 40 | return parts 41 | 42 | 43 | def purify_kwargs(kwargs): 44 | '''If type or metavar are set to None, they are removed from kwargs.''' 45 | for key, value in kwargs.copy().items(): 46 | if key in set(['type', 'metavar']) and value is None: 47 | del kwargs[key] 48 | return kwargs 49 | 50 | 51 | def find_param_docs(docstring): 52 | '''Find Sphinx's :param:, :type:, :returns:, and :rtype: lines and return 53 | a dictionary of the form: 54 | ``param: (opts, {metavar: meta, type: type, help: help})``.''' 55 | paramdocs = {} 56 | typedocs = {} 57 | for m in SPHINX_RE.finditer(docstring + '\n'): 58 | if m.group('field') in ['param', 59 | 'parameter', 60 | 'arg', 61 | 'argument', 62 | 'key', 63 | 'keyword']: 64 | # mando 65 | # :param name: Help text. name None None 0 66 | # :param name : Help text. name None 1 67 | # :param -n: Help text. -n None None 2 68 | # :param -n : Help text. -n None 3 69 | # :param --name: Help text. --name None None 4 70 | # :param --name : Help text. --name None 5 71 | # :param -n, --name: Help text. -n, --name None 6 72 | # :param -n, --name : Help text. -n, --name 7 73 | # sphinx 74 | # :param name: Help text. name None None 8 75 | # :param type name: Help text. type name None 9 76 | # :type name: str 77 | 78 | # The following is ugly, but it allows for backward compatibility 79 | 80 | if m.group('var2') is None: # 0, 2, 4, 8 81 | vname = m.group('var1') 82 | vtype = None 83 | # 1, 3, 5 84 | elif m.group('var2') is not None and '<' in m.group('var2'): 85 | vname = m.group('var1') 86 | vtype = m.group('var2') 87 | elif '-' in m.group('var1') and '-' in m.group('var2'): # 6, 7 88 | vname = '{0} {1}'.format(m.group('var1'), m.group('var2')) 89 | vtype = m.group('var3') 90 | else: # 9 91 | vname = m.group('var2') 92 | vtype = m.group('var1') 93 | 94 | name, opts, meta = get_opts('{0} {1}'.format(vname.strip(), 95 | vtype or '')) 96 | name = name.replace('-', '_') 97 | 98 | helpdoc = m.group('help').strip() 99 | helpdoc = helpdoc.splitlines(True) 100 | if len(helpdoc) > 1: 101 | helpdoc = helpdoc[0] + textwrap.dedent(''.join(helpdoc[1:])) 102 | else: 103 | helpdoc = helpdoc[0] 104 | paramdocs[name] = (opts, { 105 | 'metavar': meta or None, 106 | 'type': ARG_TYPE_MAP.get(meta.strip('<>')), 107 | 'help': helpdoc, 108 | }) 109 | elif m.group('field') == 'type': 110 | typedocs[m.group('var1').strip()] = m.group('help').strip() 111 | for key in typedocs: 112 | paramdocs[key][1]['type'] = ARG_TYPE_MAP.get(typedocs[key]) 113 | return paramdocs 114 | 115 | 116 | def get_opts(param): 117 | '''Extract options from a parameter name.''' 118 | if param.startswith('-'): 119 | opts = [] 120 | names = [] 121 | meta = None 122 | for long, name, meta in ARG_RE.findall(param): 123 | prefix = ['-', '--'][len(long)] 124 | opts.append('{0}{1}'.format(prefix, name)) 125 | names.append(name) 126 | return max(names, key=len), opts, meta 127 | opt, meta = (list(filter(None, POS_RE.findall(param))) + [''])[:2] 128 | return opt, [opt], meta 129 | 130 | 131 | def action_by_type(obj): 132 | '''Determine an action and a type for the given object if possible.''' 133 | kw = {} 134 | if isinstance(obj, bool): 135 | return {'action': ['store_true', 'store_false'][obj]} 136 | elif isinstance(obj, list): 137 | kw = {'action': 'append'} 138 | kw.update(get_type(obj)) 139 | return kw 140 | 141 | 142 | def get_type(obj): 143 | '''Determine the type of the object if among some of the built-in ones.''' 144 | otype = type(obj) 145 | if any(otype is t for t in set([int, float, str, bool])): 146 | return {'type': otype} 147 | return {} 148 | 149 | 150 | def ensure_dashes(opts): 151 | '''Ensure that the options have the right number of dashes.''' 152 | for opt in opts: 153 | if opt.startswith('-'): 154 | yield opt 155 | else: 156 | yield '-' * (1 + 1 * (len(opt) > 1)) + opt 157 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | mando: CLI interfaces for Humans! 2 | ================================= 3 | 4 | .. image:: https://img.shields.io/travis/rubik/mando 5 | :alt: Travis-CI badge 6 | :target: https://travis-ci.org/rubik/mando 7 | 8 | .. image:: https://img.shields.io/coveralls/rubik/mando 9 | :alt: Coveralls badge 10 | :target: https://coveralls.io/r/rubik/mando 11 | 12 | .. image:: https://img.shields.io/pypi/implementation/mando?label=%20&logo=python&logoColor=white 13 | :alt: PyPI - Implementation 14 | 15 | .. image:: https://img.shields.io/pypi/v/mando 16 | :alt: Latest release 17 | :target: https://pypi.python.org/pypi/mando 18 | 19 | .. image:: https://img.shields.io/pypi/l/mando 20 | :alt: PyPI - License 21 | :target: https://pypi.org/project/mando/ 22 | 23 | .. image:: https://img.shields.io/pypi/pyversions/mando 24 | :alt: PyPI - Python Version 25 | :target: https://pypi.org/project/mando/ 26 | 27 | .. image:: https://img.shields.io/pypi/format/mando 28 | :alt: Download format 29 | :target: http://pythonwheels.com/ 30 | 31 | 32 | mando is a wrapper around ``argparse``, and allows you to write complete CLI 33 | applications in seconds while maintaining all the flexibility. 34 | 35 | Installation 36 | ------------ 37 | 38 | .. code-block:: console 39 | 40 | $ pip install mando 41 | 42 | The problem 43 | ----------- 44 | 45 | While ``argparse`` is great for simple command line applications with only 46 | one, default command, when you have to add multiple commands and manage them 47 | things get really messy and long. But don't worry, mando comes to help! 48 | 49 | Quickstart 50 | ---------- 51 | 52 | .. code-block:: python 53 | 54 | from mando import command, main 55 | 56 | @command 57 | def echo(text, capitalize=False): 58 | '''Echo the given text.''' 59 | if capitalize: 60 | text = text.upper() 61 | print(text) 62 | 63 | if __name__ == '__main__': 64 | main() 65 | 66 | Generated help: 67 | 68 | .. code-block:: console 69 | 70 | $ python example.py -h 71 | usage: example.py [-h] {echo} ... 72 | 73 | positional arguments: 74 | {echo} 75 | echo Echo the given text. 76 | 77 | optional arguments: 78 | -h, --help show this help message and exit 79 | 80 | $ python example.py echo -h 81 | usage: example.py echo [-h] [--capitalize] text 82 | 83 | Echo the given text. 84 | 85 | positional arguments: 86 | text 87 | 88 | optional arguments: 89 | -h, --help show this help message and exit 90 | --capitalize 91 | 92 | Actual usage: 93 | 94 | .. code-block:: console 95 | 96 | $ python example.py echo spam 97 | spam 98 | $ python example.py echo --capitalize spam 99 | SPAM 100 | 101 | 102 | A *real* example 103 | ---------------- 104 | 105 | Something more complex and real-world-*ish*. The code: 106 | 107 | .. code-block:: python 108 | 109 | from mando import command, main 110 | 111 | 112 | @command 113 | def push(repository, all=False, dry_run=False, force=False, thin=False): 114 | '''Update remote refs along with associated objects. 115 | 116 | :param repository: Repository to push to. 117 | :param --all: Push all refs. 118 | :param -n, --dry-run: Dry run. 119 | :param -f, --force: Force updates. 120 | :param --thin: Use thin pack.''' 121 | 122 | print ('Pushing to {0}. All: {1}, dry run: {2}, force: {3}, thin: {4}' 123 | .format(repository, all, dry_run, force, thin)) 124 | 125 | 126 | if __name__ == '__main__': 127 | main() 128 | 129 | mando understands Sphinx-style ``:param:``'s in the docstring, so it creates 130 | short options and their help for you. 131 | 132 | .. code-block:: console 133 | 134 | $ python git.py push -h 135 | usage: git.py push [-h] [--all] [-n] [-f] [--thin] repository 136 | 137 | Update remote refs along with associated objects. 138 | 139 | positional arguments: 140 | repository Repository to push to. 141 | 142 | optional arguments: 143 | -h, --help show this help message and exit 144 | --all Push all refs. 145 | -n, --dry-run Dry run. 146 | -f, --force Force updates. 147 | --thin Use thin pack. 148 | 149 | Let's try it! 150 | 151 | .. code-block:: console 152 | 153 | $ python git.py push --all myrepo 154 | Pushing to myrepo. All: True, dry run: False, force: False, thin: False 155 | $ python git.py push --all -f myrepo 156 | Pushing to myrepo. All: True, dry run: False, force: True, thin: False 157 | $ python git.py push --all -fn myrepo 158 | Pushing to myrepo. All: True, dry run: True, force: True, thin: False 159 | $ python git.py push --thin -fn myrepo 160 | Pushing to myrepo. All: False, dry run: True, force: True, thin: True 161 | $ python git.py push --thin 162 | usage: git.py push [-h] [--all] [-n] [-f] [--thin] repository 163 | git.py push: error: too few arguments 164 | 165 | Amazed uh? Yes, mando got the short options and the help from the docstring! 166 | You can put much more in the docstring, and if that isn't enough, there's an 167 | ``@arg`` decorator to customize the arguments that get passed to argparse. 168 | 169 | 170 | Type annotations 171 | ---------------- 172 | 173 | mando understands Python 3-style type annotations and will warn the user if the 174 | arguments given to a command are of the wrong type. 175 | 176 | .. code-block:: python 177 | 178 | from mando import command, main 179 | 180 | 181 | @command 182 | def duplicate(string, times: int): 183 | '''Duplicate text. 184 | 185 | :param string: The text to duplicate. 186 | :param times: How many times to duplicate.''' 187 | 188 | print(string * times) 189 | 190 | 191 | if __name__ == '__main__': 192 | main() 193 | 194 | .. code-block:: console 195 | 196 | $ python3 test.py duplicate "test " 5 197 | test test test test test 198 | $ python3 test.py duplicate "test " foo 199 | usage: test.py duplicate [-h] string times 200 | test.py duplicate: error: argument times: invalid int value: 'foo' 201 | 202 | 203 | Mando has lots of other options. For example, it supports different docstring 204 | styles (Sphinx, Google and NumPy), supports shell autocompletion via the 205 | ``argcomplete`` package and supports custom format classes. For a complete 206 | documentation, visit https://mando.readthedocs.org/. 207 | -------------------------------------------------------------------------------- /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 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/mando.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/mando.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/mando" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/mando" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /mando/tests/test_core.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import pytest 3 | from mando import Program 4 | 5 | 6 | program = Program('example.py', '1.0.10') 7 | 8 | 9 | def NoopCompleter(prefix, **kwd): 10 | return [] 11 | 12 | 13 | program.option( 14 | "-f", "--foo", dest='foo', default='bar', completer=NoopCompleter, 15 | help="Real programmers don't comment their code. \ 16 | If it was hard to write, it should be hard to read." 17 | ) 18 | 19 | program.add_subprog('sub') 20 | program.sub.option( 21 | "-i", "--inc", dest='inc', type=int, default=0, 22 | help="Some help text." 23 | ) 24 | 25 | 26 | @program.command 27 | def getopt(name): 28 | ''' 29 | :param name: Name of option to return. 30 | ''' 31 | # also allows for: Script.foo 32 | return getattr(program, name) 33 | 34 | 35 | @program.sub.command 36 | def powOfSub(b, e): 37 | ''' 38 | :param b: Base. 39 | :param e: Exponent. 40 | ''' 41 | return int(b) ** int(e) + program.inc 42 | 43 | 44 | @program.sub.command('powOfSub2') 45 | def powOfSub2_impl(b, e): 46 | ''' 47 | :param b: Base. 48 | :param e: Exponent. 49 | ''' 50 | return int(b) ** int(e) - program.inc 51 | 52 | 53 | @program.command 54 | def goo(pos, verbose=False, bar=None): 55 | pass 56 | 57 | 58 | @program.command 59 | def vara(pos, foo, spam=24, *vars): 60 | ''' 61 | :param vars: Yeah, you got it right, the variable arguments. 62 | ''' 63 | pass 64 | 65 | 66 | @program.command 67 | def another(baw, owl=42, json=False, tomawk=None): 68 | '''This yet another example showcasing the power of Mando! 69 | 70 | :param baw: That's the positional argument, obviously. 71 | :param -o, --owl: Yeah, I know, this is too much. 72 | :param -j, --json: In case you want to pipe it through something. 73 | :param -t, --tomawk: Well, in this case -t isn't for time.''' 74 | pass 75 | 76 | 77 | @program.command('alias') 78 | def analiased(a, b=4): 79 | pass 80 | 81 | 82 | @program.command 83 | def power(x, y=2): 84 | return int(x) ** y 85 | 86 | 87 | @program.command('more-power') 88 | def more_power(x, y=2): 89 | '''This one really shows off complete power. 90 | 91 | :param x : Well, the base. 92 | :param -y : You got it, the exponent.''' 93 | 94 | return x ** y 95 | 96 | 97 | @program.command 98 | def repeat(what, times=10): 99 | '''Getting types from annotations. 100 | 101 | :param what: what to repeat. 102 | :param -t, --times: how many times to repeat.''' 103 | 104 | return what * times 105 | 106 | 107 | # version-agnostic way of setting annotations. 108 | # Equivalent to 'repeat(what: str, times: int=10)' 109 | repeat.__annotations__ = {'what': str, 'times': int} 110 | 111 | 112 | @program.command('more-powerful') 113 | @program.arg('x', type=int, completer=NoopCompleter) 114 | @program.arg('y', '-y', '--epsilon', type=int) 115 | def more_power_2(x, y=2): 116 | return x ** y 117 | 118 | 119 | @program.command 120 | @program.arg('x', type=int) 121 | @program.arg('y', type=int) 122 | def overriding(x, y=4): 123 | '''Yoo an override test. 124 | 125 | :param x : This is so wroong!!! Let's hope it gets overridden by @arg. 126 | :param -y : This too!!''' 127 | 128 | return x - y 129 | 130 | 131 | @program.command 132 | def dashes(a, b=5): 133 | '''Usual command help. 134 | 135 | :param a : A help obviously. 136 | :param b : Yooo.''' 137 | return a ** b 138 | 139 | 140 | @program.command 141 | def append(acc=[]): 142 | return acc 143 | 144 | 145 | GENERIC_COMMANDS_CASES = [ 146 | ('goo 2', [['2', False, None]]), 147 | ('goo 2 --verbose', [['2', True, None]]), 148 | ('goo 2 --bar 9', [['2', False, '9']]), 149 | ('goo 2 --verbose --bar 8', [['2', True, '8']]), 150 | ('vara 2 3', [['2', '3', 24]]), 151 | ('vara 2 3 --spam 8', [['2', '3', 8]]), 152 | # Unfortunately this is an argparse "bug". See: 153 | # http://bugs.python.org/issue15112 154 | # You cannot intermix positional and optional arguments for now. 155 | #('vara 1 2 --spam 8 9 8', ['1', '2', 8, '9', '8']), 156 | ('vara 1 2 4 5 --spam 8', [['1', '2', 8, '4', '5']]), 157 | ('vara --spam 8 1 2 4 5', [['1', '2', 8, '4', '5']]), 158 | ('vara 9 8 1 2 3 4', [['9', '8', 24, '1', '2', '3', '4']]), 159 | ('another 2', [['2', 42, False, None]]), 160 | ('another 2 -j', [['2', 42, True, None]]), 161 | ('another 2 -t 1 -o 3', [['2', 3, False, '1']]), 162 | ('another 2 --owl 89 --tomawk 98', [['2', 89, False, '98']]), 163 | ('another 2 --json -o 1', [['2', 1, True, None]]), 164 | ('another 3 --owl 8 --json --tomawk 8', [['3', 8, True, '8']]), 165 | ('alias 5 -b 9', [['5', 9], 'analiased']), 166 | ('more-power 9 -y 2', [[9, 2], 'more_power']), 167 | ('more-powerful 9 -y 3', [[9, 3], 'more_power_2']), 168 | ('more-powerful 9 --epsilon 3', [[9, 3], 'more_power_2']), 169 | ('overriding 2', [[2, 4]]), 170 | ('overriding 2 -y 7', [[2, 7]]), 171 | ('dashes 2', [[2, 5]]), 172 | ('dashes 8 -b 7', [[8, 7]]), 173 | ('append', [[[]]]), 174 | ('append --acc 2', [[['2']]]), 175 | ('append --acc 2 --acc 3', [[['2', '3']]]), 176 | ] 177 | 178 | 179 | @pytest.mark.parametrize('args,rest', GENERIC_COMMANDS_CASES) 180 | def test_generic_commands(args, rest): 181 | args = args.split() 182 | if len(rest) == 1: 183 | to_args = rest[0] 184 | real_name = args[0] 185 | else: 186 | to_args = rest[0] 187 | real_name = rest[1] 188 | parsed = program.parse(args) 189 | assert real_name == parsed[0].__name__ 190 | assert to_args == parsed[1] 191 | 192 | 193 | PROGRAM_EXECUTE_CASES = [ 194 | ('power 2', 4), 195 | ('power 2 -y 4', 16), 196 | ('more-power 3', 9), 197 | ('more-power 3 -y 4', 81), 198 | ('more-powerful 4 -y 2', 16), 199 | ('more-powerful 4 --epsilon 2', 16), 200 | ('overriding 2', -2), 201 | ('overriding 2 -y 7', -5), 202 | ('dashes 2', 32), 203 | ('dashes 7 -b 3', 343), 204 | ('repeat a', 'aaaaaaaaaa'), 205 | ('repeat a -t 5', 'aaaaa'), 206 | ] 207 | 208 | 209 | @pytest.mark.parametrize('args,result', PROGRAM_EXECUTE_CASES) 210 | def test_program_execute(args, result): 211 | args = args.split() 212 | assert result == program.execute(args) 213 | assert program.parse(args)[0].__name__ == program._current_command 214 | 215 | 216 | @contextmanager 217 | def does_not_raise(): 218 | yield 219 | 220 | 221 | PROGRAM_EXCEPT_CASES = [ 222 | ('repeat a', does_not_raise()), 223 | ('repeat a -t blah', pytest.raises(SystemExit)), 224 | ] 225 | 226 | 227 | @pytest.mark.parametrize('args,expectation', PROGRAM_EXCEPT_CASES) 228 | def test_program_except(args, expectation): 229 | args = args.split() 230 | 231 | with expectation: 232 | program.execute(args) 233 | 234 | assert program.parse(args)[0].__name__ == program._current_command 235 | 236 | 237 | PROGRAM_OPTIONS_CASES = [ 238 | (' getopt foo', 'bar'), 239 | (' -f xyz getopt foo', 'xyz'), 240 | ('--foo xyz getopt foo', 'xyz'), 241 | (' sub powOfSub 2 3', 8), 242 | (' -f xyz sub -i 1 powOfSub 2 3', 9), 243 | ('--foo xyz sub --inc 2 powOfSub 2 3', 10), 244 | (' sub powOfSub2 2 3', 8), 245 | (' -f xyz sub -i 1 powOfSub2 2 3', 7), 246 | ('--foo xyz sub --inc 2 powOfSub2 2 3', 6), 247 | ] 248 | 249 | 250 | @pytest.mark.parametrize('args,result', PROGRAM_OPTIONS_CASES) 251 | def test_program_options(args, result): 252 | args = args.split() 253 | assert "example.py" == program.name 254 | assert result == program.execute(args) 255 | 256 | -------------------------------------------------------------------------------- /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. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\mando.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\mando.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blacklist. They should be base names, not 14 | # paths. 15 | ignore=CVS,tests 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | 25 | [MESSAGES CONTROL] 26 | 27 | # Enable the message, report, category or checker with the given id(s). You can 28 | # either give multiple identifier separated by comma (,) or put this option 29 | # multiple time. 30 | #enable= 31 | 32 | # Disable the message, report, category or checker with the given id(s). You 33 | # can either give multiple identifier separated by comma (,) or put this option 34 | # multiple time (only on the command line, not in the configuration file where 35 | # it should appear only once). 36 | disable=W0141,W0142,E1101,E0611,C0103,W0231,W0232,E0213,E1102 37 | 38 | 39 | [REPORTS] 40 | 41 | # Set the output format. Available formats are text, parseable, colorized, msvs 42 | # (visual studio) and html 43 | output-format=colorized 44 | 45 | # Include message's id in output 46 | include-ids=yes 47 | 48 | # Put messages in a separate file for each module / package specified on the 49 | # command line instead of printing them on stdout. Reports (if any) will be 50 | # written in a file name "pylint_global.[txt|html]". 51 | files-output=no 52 | 53 | # Tells whether to display a full report or only the messages 54 | reports=yes 55 | 56 | # Python expression which should return a note less than 10 (10 is the highest 57 | # note). You have access to the variables errors warning, statement which 58 | # respectively contain the number of errors / warnings messages and the total 59 | # number of statements analyzed. This is used by the global evaluation report 60 | # (RP0004). 61 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 62 | 63 | # Add a comment according to your evaluation note. This is used by the global 64 | # evaluation report (RP0004). 65 | comment=no 66 | 67 | 68 | [BASIC] 69 | 70 | # Required attributes for module, separated by a comma 71 | required-attributes= 72 | 73 | # List of builtins function names that should not be used, separated by a comma 74 | bad-functions=map,filter,apply,input 75 | 76 | # Regular expression which should only match correct module names 77 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 78 | 79 | # Regular expression which should only match correct module level names 80 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 81 | 82 | # Regular expression which should only match correct class names 83 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 84 | 85 | # Regular expression which should only match correct function names 86 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 87 | 88 | # Regular expression which should only match correct method names 89 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 90 | 91 | # Regular expression which should only match correct instance attribute names 92 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 93 | 94 | # Regular expression which should only match correct argument names 95 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 96 | 97 | # Regular expression which should only match correct variable names 98 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 99 | 100 | # Regular expression which should only match correct list comprehension / 101 | # generator expression variable names 102 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 103 | 104 | # Good variable names which should always be accepted, separated by a comma 105 | good-names=i,j,k,ex,Run,_ 106 | 107 | # Bad variable names which should always be refused, separated by a comma 108 | bad-names=foo,bar,baz,toto,tutu,tata 109 | 110 | # Regular expression which should only match functions or classes name which do 111 | # not require a docstring 112 | no-docstring-rgx=__.*__ 113 | 114 | 115 | [TYPECHECK] 116 | 117 | # Tells whether missing members accessed in mixin class should be ignored. A 118 | # mixin class is detected if its name ends with "mixin" (case insensitive). 119 | ignore-mixin-members=yes 120 | 121 | # List of classes names for which member attributes should not be checked 122 | # (useful for classes with attributes dynamically set). 123 | ignored-classes=SQLObject 124 | 125 | # When zope mode is activated, add a predefined set of Zope acquired attributes 126 | # to generated-members. 127 | zope=no 128 | 129 | # List of members which are set dynamically and missed by pylint inference 130 | # system, and so shouldn't trigger E0201 when accessed. Python regular 131 | # expressions are accepted. 132 | generated-members=REQUEST,acl_users,aq_parent 133 | 134 | 135 | [FORMAT] 136 | 137 | # Maximum number of characters on a single line. 138 | max-line-length=80 139 | 140 | # Maximum number of lines in a module 141 | max-module-lines=1000 142 | 143 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 144 | # tab). 145 | indent-string=' ' 146 | 147 | 148 | [SIMILARITIES] 149 | 150 | # Minimum lines number of a similarity. 151 | min-similarity-lines=4 152 | 153 | # Ignore comments when computing similarities. 154 | ignore-comments=yes 155 | 156 | # Ignore docstrings when computing similarities. 157 | ignore-docstrings=yes 158 | 159 | 160 | [MISCELLANEOUS] 161 | 162 | # List of note tags to take in consideration, separated by a comma. 163 | notes=FIXME,XXX,TODO 164 | 165 | 166 | [VARIABLES] 167 | 168 | # Tells whether we should check for unused import in __init__ files. 169 | init-import=no 170 | 171 | # A regular expression matching the beginning of the name of dummy variables 172 | # (i.e. not used). 173 | dummy-variables-rgx=_|dummy 174 | 175 | # List of additional names supposed to be defined in builtins. Remember that 176 | # you should avoid to define new builtins when possible. 177 | additional-builtins= 178 | 179 | 180 | [CLASSES] 181 | 182 | # List of interface methods to ignore, separated by a comma. This is used for 183 | # instance to not check methods defines in Zope's Interface base class. 184 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 185 | 186 | # List of method names used to declare (i.e. assign) instance attributes. 187 | defining-attr-methods=__init__,__new__,setUp 188 | 189 | # List of valid names for the first argument in a class method. 190 | valid-classmethod-first-arg=cls 191 | 192 | 193 | [DESIGN] 194 | 195 | # Maximum number of arguments for function / method 196 | max-args=5 197 | 198 | # Argument names that match this expression will be ignored. Default to name 199 | # with leading underscore 200 | ignored-argument-names=_.* 201 | 202 | # Maximum number of locals for function / method body 203 | max-locals=15 204 | 205 | # Maximum number of return / yield for function / method body 206 | max-returns=6 207 | 208 | # Maximum number of branch for function / method body 209 | max-branchs=12 210 | 211 | # Maximum number of statements in function / method body 212 | max-statements=50 213 | 214 | # Maximum number of parents for a class (see R0901). 215 | max-parents=7 216 | 217 | # Maximum number of attributes for a class (see R0902). 218 | max-attributes=7 219 | 220 | # Minimum number of public methods for a class (see R0903). 221 | min-public-methods=2 222 | 223 | # Maximum number of public methods for a class (see R0904). 224 | max-public-methods=20 225 | 226 | 227 | [IMPORTS] 228 | 229 | # Deprecated modules which should not be used, separated by a comma 230 | deprecated-modules=regsub,string,TERMIOS,Bastion,rexec 231 | 232 | # Create a graph of every (i.e. internal and external) dependencies in the 233 | # given file (report RP0402 must not be disabled) 234 | import-graph= 235 | 236 | # Create a graph of external dependencies in the given file (report RP0402 must 237 | # not be disabled) 238 | ext-import-graph= 239 | 240 | # Create a graph of internal dependencies in the given file (report RP0402 must 241 | # not be disabled) 242 | int-import-graph= 243 | 244 | 245 | [EXCEPTIONS] 246 | 247 | # Exceptions that will emit a warning when being caught. Defaults to 248 | # "Exception" 249 | overgeneral-exceptions=Exception 250 | -------------------------------------------------------------------------------- /mando/napoleon/iterators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | sphinx.ext.napoleon.iterators 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | 7 | A collection of helpful iterators. 8 | 9 | 10 | :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. 11 | :license: BSD, see LICENSE for details. 12 | """ 13 | 14 | import collections 15 | 16 | 17 | class peek_iter: 18 | """An iterator object that supports peeking ahead. 19 | 20 | Parameters 21 | ---------- 22 | o : iterable or callable 23 | `o` is interpreted very differently depending on the presence of 24 | `sentinel`. 25 | 26 | If `sentinel` is not given, then `o` must be a collection object 27 | which supports either the iteration protocol or the sequence protocol. 28 | 29 | If `sentinel` is given, then `o` must be a callable object. 30 | 31 | sentinel : any value, optional 32 | If given, the iterator will call `o` with no arguments for each 33 | call to its `next` method; if the value returned is equal to 34 | `sentinel`, :exc:`StopIteration` will be raised, otherwise the 35 | value will be returned. 36 | 37 | See Also 38 | -------- 39 | `peek_iter` can operate as a drop in replacement for the built-in 40 | `iter `_ function. 41 | 42 | Attributes 43 | ---------- 44 | sentinel 45 | The value used to indicate the iterator is exhausted. If `sentinel` 46 | was not given when the `peek_iter` was instantiated, then it will 47 | be set to a new object instance: ``object()``. 48 | 49 | """ 50 | def __init__(self, *args): 51 | # type: (Any) -> None 52 | """__init__(o, sentinel=None)""" 53 | self._iterable = iter(*args) # type: Iterable 54 | self._cache = collections.deque() # type: collections.deque 55 | if len(args) == 2: 56 | self.sentinel = args[1] 57 | else: 58 | self.sentinel = object() 59 | 60 | def __iter__(self): 61 | # type: () -> peek_iter 62 | return self 63 | 64 | def __next__(self, n=None): 65 | # type: (int) -> Any 66 | # note: prevent 2to3 to transform self.next() in next(self) which 67 | # causes an infinite loop ! 68 | return getattr(self, 'next')(n) 69 | 70 | def _fillcache(self, n): 71 | # type: (int) -> None 72 | """Cache `n` items. If `n` is 0 or None, then 1 item is cached.""" 73 | if not n: 74 | n = 1 75 | try: 76 | while len(self._cache) < n: 77 | self._cache.append(next(self._iterable)) # type: ignore 78 | except StopIteration: 79 | while len(self._cache) < n: 80 | self._cache.append(self.sentinel) 81 | 82 | def has_next(self): 83 | # type: () -> bool 84 | """Determine if iterator is exhausted. 85 | 86 | Returns 87 | ------- 88 | bool 89 | True if iterator has more items, False otherwise. 90 | 91 | Note 92 | ---- 93 | Will never raise :exc:`StopIteration`. 94 | 95 | """ 96 | return self.peek() != self.sentinel 97 | 98 | def next(self, n=None): 99 | # type: (int) -> Any 100 | """Get the next item or `n` items of the iterator. 101 | 102 | Parameters 103 | ---------- 104 | n : int or None 105 | The number of items to retrieve. Defaults to None. 106 | 107 | Returns 108 | ------- 109 | item or list of items 110 | The next item or `n` items of the iterator. If `n` is None, the 111 | item itself is returned. If `n` is an int, the items will be 112 | returned in a list. If `n` is 0, an empty list is returned. 113 | 114 | Raises 115 | ------ 116 | StopIteration 117 | Raised if the iterator is exhausted, even if `n` is 0. 118 | 119 | """ 120 | self._fillcache(n) 121 | if not n: 122 | if self._cache[0] == self.sentinel: 123 | raise StopIteration 124 | if n is None: 125 | result = self._cache.popleft() 126 | else: 127 | result = [] 128 | else: 129 | if self._cache[n - 1] == self.sentinel: 130 | raise StopIteration 131 | result = [self._cache.popleft() for i in range(n)] 132 | return result 133 | 134 | def peek(self, n=None): 135 | # type: (int) -> Any 136 | """Preview the next item or `n` items of the iterator. 137 | 138 | The iterator is not advanced when peek is called. 139 | 140 | Returns 141 | ------- 142 | item or list of items 143 | The next item or `n` items of the iterator. If `n` is None, the 144 | item itself is returned. If `n` is an int, the items will be 145 | returned in a list. If `n` is 0, an empty list is returned. 146 | 147 | If the iterator is exhausted, `peek_iter.sentinel` is returned, 148 | or placed as the last item in the returned list. 149 | 150 | Note 151 | ---- 152 | Will never raise :exc:`StopIteration`. 153 | 154 | """ 155 | self._fillcache(n) 156 | if n is None: 157 | result = self._cache[0] 158 | else: 159 | result = [self._cache[i] for i in range(n)] 160 | return result 161 | 162 | 163 | class modify_iter(peek_iter): 164 | """An iterator object that supports modifying items as they are returned. 165 | 166 | Parameters 167 | ---------- 168 | o : iterable or callable 169 | `o` is interpreted very differently depending on the presence of 170 | `sentinel`. 171 | 172 | If `sentinel` is not given, then `o` must be a collection object 173 | which supports either the iteration protocol or the sequence protocol. 174 | 175 | If `sentinel` is given, then `o` must be a callable object. 176 | 177 | sentinel : any value, optional 178 | If given, the iterator will call `o` with no arguments for each 179 | call to its `next` method; if the value returned is equal to 180 | `sentinel`, :exc:`StopIteration` will be raised, otherwise the 181 | value will be returned. 182 | 183 | modifier : callable, optional 184 | The function that will be used to modify each item returned by the 185 | iterator. `modifier` should take a single argument and return a 186 | single value. Defaults to ``lambda x: x``. 187 | 188 | If `sentinel` is not given, `modifier` must be passed as a keyword 189 | argument. 190 | 191 | Attributes 192 | ---------- 193 | modifier : callable 194 | `modifier` is called with each item in `o` as it is iterated. The 195 | return value of `modifier` is returned in lieu of the item. 196 | 197 | Values returned by `peek` as well as `next` are affected by 198 | `modifier`. However, `modify_iter.sentinel` is never passed through 199 | `modifier`; it will always be returned from `peek` unmodified. 200 | 201 | Example 202 | ------- 203 | >>> a = [" A list ", 204 | ... " of strings ", 205 | ... " with ", 206 | ... " extra ", 207 | ... " whitespace. "] 208 | >>> modifier = lambda s: s.strip().replace('with', 'without') 209 | >>> for s in modify_iter(a, modifier=modifier): 210 | ... print('"%s"' % s) 211 | "A list" 212 | "of strings" 213 | "without" 214 | "extra" 215 | "whitespace." 216 | 217 | """ 218 | def __init__(self, *args, **kwargs): 219 | # type: (Any, Any) -> None 220 | """__init__(o, sentinel=None, modifier=lambda x: x)""" 221 | if 'modifier' in kwargs: 222 | self.modifier = kwargs['modifier'] 223 | elif len(args) > 2: 224 | self.modifier = args[2] 225 | args = args[:2] 226 | else: 227 | self.modifier = lambda x: x 228 | if not callable(self.modifier): 229 | raise TypeError('modify_iter(o, modifier): ' 230 | 'modifier must be callable') 231 | super(modify_iter, self).__init__(*args) 232 | 233 | def _fillcache(self, n): 234 | # type: (int) -> None 235 | """Cache `n` modified items. If `n` is 0 or None, 1 item is cached. 236 | 237 | Each item returned by the iterator is passed through the 238 | `modify_iter.modified` function before being cached. 239 | 240 | """ 241 | if not n: 242 | n = 1 243 | try: 244 | while len(self._cache) < n: 245 | self._cache.append(self.modifier(next(self._iterable))) # type: ignore 246 | except StopIteration: 247 | while len(self._cache) < n: 248 | self._cache.append(self.sentinel) 249 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # mando documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Dec 4 15:37:28 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # 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 sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.coverage', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = '.rst' 41 | 42 | # The encoding of source files. 43 | #source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'mando' 50 | copyright = '2013, Michele Lacchia' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = '0.2' 58 | # The full version, including alpha/beta/rc tags. 59 | release = '0.2' 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | #language = None 64 | 65 | # There are two options for replacing |today|: either, you set today to some 66 | # non-false value, then it is used: 67 | #today = '' 68 | # Else, today_fmt is used as the format for a strftime call. 69 | #today_fmt = '%B %d, %Y' 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | exclude_patterns = ['_build'] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all 76 | # documents. 77 | #default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | #add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | #add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | #show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = 'sphinx' 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | #modindex_common_prefix = [] 95 | 96 | # If true, keep warnings as "system message" paragraphs in the built documents. 97 | #keep_warnings = False 98 | 99 | 100 | # -- Options for HTML output ---------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | html_theme = 'default' 105 | 106 | # Theme options are theme-specific and customize the look and feel of a theme 107 | # further. For a list of options available for each theme, see the 108 | # documentation. 109 | #html_theme_options = {} 110 | 111 | # Add any paths that contain custom themes here, relative to this directory. 112 | #html_theme_path = [] 113 | 114 | # The name for this set of Sphinx documents. If None, it defaults to 115 | # " v documentation". 116 | #html_title = None 117 | 118 | # A shorter title for the navigation bar. Default is the same as html_title. 119 | #html_short_title = None 120 | 121 | # The name of an image file (relative to this directory) to place at the top 122 | # of the sidebar. 123 | #html_logo = None 124 | 125 | # The name of an image file (within the static path) to use as favicon of the 126 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 127 | # pixels large. 128 | #html_favicon = None 129 | 130 | # Add any paths that contain custom static files (such as style sheets) here, 131 | # relative to this directory. They are copied after the builtin static files, 132 | # so a file named "default.css" will overwrite the builtin "default.css". 133 | html_static_path = ['_static'] 134 | 135 | # Add any extra paths that contain custom files (such as robots.txt or 136 | # .htaccess) here, relative to this directory. These files are copied 137 | # directly to the root of the documentation. 138 | #html_extra_path = [] 139 | 140 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 141 | # using the given strftime format. 142 | #html_last_updated_fmt = '%b %d, %Y' 143 | 144 | # If true, SmartyPants will be used to convert quotes and dashes to 145 | # typographically correct entities. 146 | #html_use_smartypants = True 147 | 148 | # Custom sidebar templates, maps document names to template names. 149 | #html_sidebars = {} 150 | 151 | # Additional templates that should be rendered to pages, maps page names to 152 | # template names. 153 | #html_additional_pages = {} 154 | 155 | # If false, no module index is generated. 156 | #html_domain_indices = True 157 | 158 | # If false, no index is generated. 159 | #html_use_index = True 160 | 161 | # If true, the index is split into individual pages for each letter. 162 | #html_split_index = False 163 | 164 | # If true, links to the reST sources are added to the pages. 165 | #html_show_sourcelink = True 166 | 167 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 168 | #html_show_sphinx = True 169 | 170 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 171 | #html_show_copyright = True 172 | 173 | # If true, an OpenSearch description file will be output, and all pages will 174 | # contain a tag referring to it. The value of this option must be the 175 | # base URL from which the finished HTML is served. 176 | #html_use_opensearch = '' 177 | 178 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 179 | #html_file_suffix = None 180 | 181 | # Output file base name for HTML help builder. 182 | htmlhelp_basename = 'mandodoc' 183 | 184 | 185 | # -- Options for LaTeX output --------------------------------------------- 186 | 187 | latex_elements = { 188 | # The paper size ('letterpaper' or 'a4paper'). 189 | #'papersize': 'letterpaper', 190 | 191 | # The font size ('10pt', '11pt' or '12pt'). 192 | #'pointsize': '10pt', 193 | 194 | # Additional stuff for the LaTeX preamble. 195 | #'preamble': '', 196 | } 197 | 198 | # Grouping the document tree into LaTeX files. List of tuples 199 | # (source start file, target name, title, 200 | # author, documentclass [howto, manual, or own class]). 201 | latex_documents = [ 202 | ('index', 'mando.tex', 'mando Documentation', 203 | 'Michele Lacchia', 'manual'), 204 | ] 205 | 206 | # The name of an image file (relative to this directory) to place at the top of 207 | # the title page. 208 | #latex_logo = None 209 | 210 | # For "manual" documents, if this is true, then toplevel headings are parts, 211 | # not chapters. 212 | #latex_use_parts = False 213 | 214 | # If true, show page references after internal links. 215 | #latex_show_pagerefs = False 216 | 217 | # If true, show URL addresses after external links. 218 | #latex_show_urls = False 219 | 220 | # Documents to append as an appendix to all manuals. 221 | #latex_appendices = [] 222 | 223 | # If false, no module index is generated. 224 | #latex_domain_indices = True 225 | 226 | 227 | # -- Options for manual page output --------------------------------------- 228 | 229 | # One entry per manual page. List of tuples 230 | # (source start file, name, description, authors, manual section). 231 | man_pages = [ 232 | ('index', 'mando', 'mando Documentation', 233 | ['Michele Lacchia'], 1) 234 | ] 235 | 236 | # If true, show URL addresses after external links. 237 | #man_show_urls = False 238 | 239 | 240 | # -- Options for Texinfo output ------------------------------------------- 241 | 242 | # Grouping the document tree into Texinfo files. List of tuples 243 | # (source start file, target name, title, author, 244 | # dir menu entry, description, category) 245 | texinfo_documents = [ 246 | ('index', 'mando', 'mando Documentation', 247 | 'Michele Lacchia', 'mando', 'One line description of project.', 248 | 'Miscellaneous'), 249 | ] 250 | 251 | # Documents to append as an appendix to all manuals. 252 | #texinfo_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | #texinfo_domain_indices = True 256 | 257 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 258 | #texinfo_show_urls = 'footnote' 259 | 260 | # If true, do not generate a @detailmenu in the "Top" node's menu. 261 | #texinfo_no_detailmenu = False 262 | -------------------------------------------------------------------------------- /mando/core.py: -------------------------------------------------------------------------------- 1 | '''Main module containing the class Program(), which allows the conversion from 2 | ordinary Python functions into commands for the command line. It uses 3 | :py:module:``argparse`` behind the scenes.''' 4 | 5 | import argparse 6 | import inspect 7 | import sys 8 | from inspect import signature 9 | 10 | from mando.napoleon import Config, GoogleDocstring, NumpyDocstring 11 | 12 | from mando.utils import (purify_doc, action_by_type, find_param_docs, 13 | split_doc, ensure_dashes, purify_kwargs) 14 | 15 | 16 | _POSITIONAL = type('_positional', (object,), {}) 17 | _DISPATCH_TO = '_dispatch_to' 18 | 19 | 20 | class SubProgram: 21 | def __init__(self, parser, signatures): 22 | self.parser = parser 23 | self._subparsers = self.parser.add_subparsers() 24 | self._signatures = signatures 25 | 26 | @property 27 | def name(self): 28 | return self.parser.prog 29 | 30 | # Add global script options. 31 | def option(self, *args, **kwd): 32 | assert args and all(arg.startswith('-') for arg in args), \ 33 | "Positional arguments not supported here" 34 | completer = kwd.pop('completer', None) 35 | arg = self.parser.add_argument(*args, **kwd) 36 | if completer is not None: 37 | arg.completer = completer 38 | # do not attempt to shadow existing attributes 39 | assert not hasattr(self, arg.dest), "Invalid option name: " + arg.dest 40 | return arg 41 | 42 | def add_subprog(self, name, **kwd): 43 | # also always provide help= to fix missing entry in command list 44 | help = kwd.pop('help', "{} subcommand".format(name)) 45 | prog = SubProgram(self._subparsers.add_parser(name, help=help, **kwd), 46 | self._signatures) 47 | # do not attempt to overwrite existing attributes 48 | assert not hasattr(self, name), "Invalid sub-prog name: " + name 49 | setattr(self, name, prog) 50 | return prog 51 | 52 | def command(self, *args, **kwargs): 53 | '''A decorator to convert a function into a command. It can be applied 54 | as ``@command`` or as ``@command(new_name)``, specifying an alternative 55 | name for the command (default one is ``func.__name__``).''' 56 | if len(args) == 1 and hasattr(args[0], '__call__'): 57 | return self._generate_command(args[0]) 58 | else: 59 | def _command(func): 60 | return self._generate_command(func, *args, **kwargs) 61 | return _command 62 | 63 | def arg(self, param, *args, **kwargs): 64 | '''A decorator to override the parameters extracted from the docstring 65 | or to add new ones. 66 | 67 | :param param: The parameter's name. It must be among the function's 68 | arguments names.''' 69 | def wrapper(func): 70 | if not hasattr(func, '_argopts'): 71 | func._argopts = {} 72 | func._argopts[param] = (args, kwargs) 73 | return func 74 | return wrapper 75 | 76 | def _generate_command(self, func, name=None, doctype='rest', 77 | *args, **kwargs): 78 | '''Generate argparse's subparser. 79 | 80 | :param func: The function to analyze. 81 | :param name: If given, a different name for the command. The default 82 | one is ``func.__name__``.''' 83 | 84 | name = name or func.__name__ 85 | doc = (inspect.getdoc(func) or '').strip() + '\n' 86 | 87 | if doctype == 'numpy': 88 | config = Config(napoleon_google_docstring=False, 89 | napoleon_use_rtype=False) 90 | doc = str(NumpyDocstring(doc, config)) 91 | elif doctype == 'google': 92 | config = Config(napoleon_numpy_docstring=False, 93 | napoleon_use_rtype=False) 94 | doc = str(GoogleDocstring(doc, config)) 95 | elif doctype == 'rest': 96 | pass 97 | else: 98 | raise ValueError('doctype must be one of "numpy", "google", ' 99 | 'or "rest"') 100 | cmd_help, cmd_desc = split_doc(purify_doc(doc)) 101 | subparser = self._subparsers.add_parser(name, 102 | help=cmd_help or None, 103 | description=cmd_desc or None, 104 | **kwargs) 105 | 106 | doc_params = find_param_docs(doc) 107 | self._signatures[func.__name__] = signature(func) 108 | 109 | for a, kw in self._analyze_func(func, doc_params): 110 | completer = kw.pop('completer', None) 111 | arg = subparser.add_argument(*a, **purify_kwargs(kw)) 112 | if completer is not None: 113 | arg.completer = completer 114 | 115 | subparser.set_defaults(**{_DISPATCH_TO: func}) 116 | return func 117 | 118 | def _analyze_func(self, func, doc_params): 119 | '''Analyze the given function, merging default arguments, overridden 120 | arguments (with @arg) and parameters extracted from the docstring. 121 | 122 | :param func: The function to analyze. 123 | :param doc_params: Parameters extracted from docstring. 124 | ''' 125 | 126 | # prevent unnecessary inspect calls 127 | sig = self._signatures.get(func.__name__) or signature(func) 128 | overrides = getattr(func, '_argopts', {}) 129 | for name, param in sig.parameters.items(): 130 | 131 | if param.kind is param.VAR_POSITIONAL: 132 | kwargs = {'nargs': '*'} 133 | kwargs.update(doc_params.get(name, (None, {}))[1]) 134 | yield ([name], kwargs) 135 | continue 136 | 137 | default = param.default 138 | if default is sig.empty: 139 | default = _POSITIONAL() 140 | 141 | opts, meta = doc_params.get(name, ([], {})) 142 | # check docstring for type first, then type annotation 143 | if meta.get('type') is None and param.annotation is not sig.empty: 144 | meta['type'] = param.annotation 145 | 146 | override = overrides.get(name, ((), {})) 147 | yield merge(name, default, override, opts, meta) 148 | 149 | 150 | class Program(SubProgram): 151 | def __init__(self, prog=None, version=None, **kwargs): 152 | parser = argparse.ArgumentParser(prog, **kwargs) 153 | if version is not None: 154 | parser.add_argument('-v', '--version', action='version', 155 | version=version) 156 | 157 | super(Program, self).__init__(parser, dict()) 158 | self._options = None 159 | self._current_command = None 160 | 161 | # Attribute lookup fallback redirecting to (internal) options instance. 162 | def __getattr__(self, attr): 163 | return getattr(self._options, attr) 164 | 165 | def parse(self, args): 166 | '''Parse the given arguments and return a tuple ``(command, args)``, 167 | where ``args`` is a list consisting of all arguments. The command can 168 | then be called as ``command(*args)``. 169 | 170 | :param args: The arguments to parse.''' 171 | try: 172 | # run completion handler before parsing 173 | import argcomplete 174 | argcomplete.autocomplete(self.parser) 175 | except ImportError: # pragma: no cover 176 | # ignore error if not installed 177 | pass 178 | 179 | self._options = self.parser.parse_args(args) 180 | arg_map = self._options.__dict__ 181 | if _DISPATCH_TO not in arg_map: # pragma: no cover 182 | self.parser.error("too few arguments") 183 | 184 | command = arg_map.pop(_DISPATCH_TO) 185 | sig = self._signatures[command.__name__] 186 | real_args = [] 187 | for name, arg in sig.parameters.items(): 188 | if arg.kind is arg.VAR_POSITIONAL: 189 | if arg_map.get(name): 190 | real_args.extend(arg_map.pop(name)) 191 | else: 192 | real_args.append(arg_map.pop(name)) 193 | return command, real_args 194 | 195 | def execute(self, args): 196 | '''Parse the arguments and execute the resulting command. 197 | 198 | :param args: The arguments to parse.''' 199 | command, a = self.parse(args) 200 | self._current_command = command.__name__ 201 | return command(*a) 202 | 203 | def __call__(self): # pragma: no cover 204 | '''Parse ``sys.argv`` and execute the resulting command.''' 205 | return self.execute(sys.argv[1:]) 206 | 207 | 208 | def merge(arg, default, override, args, kwargs): 209 | '''Merge all the possible arguments into a tuple and a dictionary. 210 | 211 | :param arg: The argument's name. 212 | :param default: The argument's default value or an instance of _POSITIONAL. 213 | :param override: A tuple containing (args, kwargs) given to @arg. 214 | :param args: The arguments extracted from the docstring. 215 | :param kwargs: The keyword arguments extracted from the docstring.''' 216 | opts = [arg] 217 | if not isinstance(default, _POSITIONAL): 218 | opts = list(ensure_dashes(args or opts)) 219 | kwargs.update({'default': default, 'dest': arg}) 220 | kwargs.update(action_by_type(default)) 221 | else: 222 | # positionals can't have a metavar, otherwise the help is screwed 223 | # if one really wants the metavar, it can be added with @arg 224 | kwargs['metavar'] = None 225 | kwargs.update(override[1]) 226 | return override[0] or opts, kwargs 227 | -------------------------------------------------------------------------------- /mando/napoleon/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | sphinx.ext.napoleon 4 | ~~~~~~~~~~~~~~~~~~~ 5 | 6 | Support for NumPy and Google style docstrings. 7 | 8 | :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. 9 | :license: BSD, see LICENSE for details. 10 | """ 11 | 12 | from mando.napoleon.docstring import GoogleDocstring, NumpyDocstring 13 | 14 | 15 | class Config: 16 | """Sphinx napoleon extension settings in `conf.py`. 17 | 18 | Listed below are all the settings used by napoleon and their default 19 | values. These settings can be changed in the Sphinx `conf.py` file. Make 20 | sure that both "sphinx.ext.autodoc" and "sphinx.ext.napoleon" are 21 | enabled in `conf.py`:: 22 | 23 | # conf.py 24 | 25 | # Add any Sphinx extension module names here, as strings 26 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon'] 27 | 28 | # Napoleon settings 29 | napoleon_google_docstring = True 30 | napoleon_numpy_docstring = True 31 | napoleon_include_init_with_doc = False 32 | napoleon_include_private_with_doc = False 33 | napoleon_include_special_with_doc = False 34 | napoleon_use_admonition_for_examples = False 35 | napoleon_use_admonition_for_notes = False 36 | napoleon_use_admonition_for_references = False 37 | napoleon_use_ivar = False 38 | napoleon_use_param = True 39 | napoleon_use_rtype = True 40 | napoleon_use_keyword = True 41 | 42 | .. _Google style: 43 | http://google.github.io/styleguide/pyguide.html 44 | .. _NumPy style: 45 | https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt 46 | 47 | Attributes 48 | ---------- 49 | napoleon_google_docstring : :obj:`bool` (Defaults to True) 50 | True to parse `Google style`_ docstrings. False to disable support 51 | for Google style docstrings. 52 | napoleon_numpy_docstring : :obj:`bool` (Defaults to True) 53 | True to parse `NumPy style`_ docstrings. False to disable support 54 | for NumPy style docstrings. 55 | napoleon_include_init_with_doc : :obj:`bool` (Defaults to False) 56 | True to list ``__init___`` docstrings separately from the class 57 | docstring. False to fall back to Sphinx's default behavior, which 58 | considers the ``__init___`` docstring as part of the class 59 | documentation. 60 | 61 | **If True**:: 62 | 63 | def __init__(self): 64 | \"\"\" 65 | This will be included in the docs because it has a docstring 66 | \"\"\" 67 | 68 | def __init__(self): 69 | # This will NOT be included in the docs 70 | 71 | napoleon_include_private_with_doc : :obj:`bool` (Defaults to False) 72 | True to include private members (like ``_membername``) with docstrings 73 | in the documentation. False to fall back to Sphinx's default behavior. 74 | 75 | **If True**:: 76 | 77 | def _included(self): 78 | \"\"\" 79 | This will be included in the docs because it has a docstring 80 | \"\"\" 81 | pass 82 | 83 | def _skipped(self): 84 | # This will NOT be included in the docs 85 | pass 86 | 87 | napoleon_include_special_with_doc : :obj:`bool` (Defaults to False) 88 | True to include special members (like ``__membername__``) with 89 | docstrings in the documentation. False to fall back to Sphinx's 90 | default behavior. 91 | 92 | **If True**:: 93 | 94 | def __str__(self): 95 | \"\"\" 96 | This will be included in the docs because it has a docstring 97 | \"\"\" 98 | return unicode(self).encode('utf-8') 99 | 100 | def __unicode__(self): 101 | # This will NOT be included in the docs 102 | return unicode(self.__class__.__name__) 103 | 104 | napoleon_use_admonition_for_examples : :obj:`bool` (Defaults to False) 105 | True to use the ``.. admonition::`` directive for the **Example** and 106 | **Examples** sections. False to use the ``.. rubric::`` directive 107 | instead. One may look better than the other depending on what HTML 108 | theme is used. 109 | 110 | This `NumPy style`_ snippet will be converted as follows:: 111 | 112 | Example 113 | ------- 114 | This is just a quick example 115 | 116 | **If True**:: 117 | 118 | .. admonition:: Example 119 | 120 | This is just a quick example 121 | 122 | **If False**:: 123 | 124 | .. rubric:: Example 125 | 126 | This is just a quick example 127 | 128 | napoleon_use_admonition_for_notes : :obj:`bool` (Defaults to False) 129 | True to use the ``.. admonition::`` directive for **Notes** sections. 130 | False to use the ``.. rubric::`` directive instead. 131 | 132 | Note 133 | ---- 134 | The singular **Note** section will always be converted to a 135 | ``.. note::`` directive. 136 | 137 | See Also 138 | -------- 139 | :attr:`napoleon_use_admonition_for_examples` 140 | 141 | napoleon_use_admonition_for_references : :obj:`bool` (Defaults to False) 142 | True to use the ``.. admonition::`` directive for **References** 143 | sections. False to use the ``.. rubric::`` directive instead. 144 | 145 | See Also 146 | -------- 147 | :attr:`napoleon_use_admonition_for_examples` 148 | 149 | napoleon_use_ivar : :obj:`bool` (Defaults to False) 150 | True to use the ``:ivar:`` role for instance variables. False to use 151 | the ``.. attribute::`` directive instead. 152 | 153 | This `NumPy style`_ snippet will be converted as follows:: 154 | 155 | Attributes 156 | ---------- 157 | attr1 : int 158 | Description of `attr1` 159 | 160 | **If True**:: 161 | 162 | :ivar attr1: Description of `attr1` 163 | :vartype attr1: int 164 | 165 | **If False**:: 166 | 167 | .. attribute:: attr1 168 | 169 | *int* 170 | 171 | Description of `attr1` 172 | 173 | napoleon_use_param : :obj:`bool` (Defaults to True) 174 | True to use a ``:param:`` role for each function parameter. False to 175 | use a single ``:parameters:`` role for all the parameters. 176 | 177 | This `NumPy style`_ snippet will be converted as follows:: 178 | 179 | Parameters 180 | ---------- 181 | arg1 : str 182 | Description of `arg1` 183 | arg2 : int, optional 184 | Description of `arg2`, defaults to 0 185 | 186 | **If True**:: 187 | 188 | :param arg1: Description of `arg1` 189 | :type arg1: str 190 | :param arg2: Description of `arg2`, defaults to 0 191 | :type arg2: int, optional 192 | 193 | **If False**:: 194 | 195 | :parameters: * **arg1** (*str*) -- 196 | Description of `arg1` 197 | * **arg2** (*int, optional*) -- 198 | Description of `arg2`, defaults to 0 199 | 200 | napoleon_use_keyword : :obj:`bool` (Defaults to True) 201 | True to use a ``:keyword:`` role for each function keyword argument. 202 | False to use a single ``:keyword arguments:`` role for all the 203 | keywords. 204 | 205 | This behaves similarly to :attr:`napoleon_use_param`. Note unlike 206 | docutils, ``:keyword:`` and ``:param:`` will not be treated the same 207 | way - there will be a separate "Keyword Arguments" section, rendered 208 | in the same fashion as "Parameters" section (type links created if 209 | possible) 210 | 211 | See Also 212 | -------- 213 | :attr:`napoleon_use_param` 214 | 215 | napoleon_use_rtype : :obj:`bool` (Defaults to True) 216 | True to use the ``:rtype:`` role for the return type. False to output 217 | the return type inline with the description. 218 | 219 | This `NumPy style`_ snippet will be converted as follows:: 220 | 221 | Returns 222 | ------- 223 | bool 224 | True if successful, False otherwise 225 | 226 | **If True**:: 227 | 228 | :returns: True if successful, False otherwise 229 | :rtype: bool 230 | 231 | **If False**:: 232 | 233 | :returns: *bool* -- True if successful, False otherwise 234 | 235 | """ 236 | _config_values = { 237 | 'napoleon_google_docstring': (True, 'env'), 238 | 'napoleon_numpy_docstring': (True, 'env'), 239 | 'napoleon_include_init_with_doc': (False, 'env'), 240 | 'napoleon_include_private_with_doc': (False, 'env'), 241 | 'napoleon_include_special_with_doc': (False, 'env'), 242 | 'napoleon_use_admonition_for_examples': (False, 'env'), 243 | 'napoleon_use_admonition_for_notes': (False, 'env'), 244 | 'napoleon_use_admonition_for_references': (False, 'env'), 245 | 'napoleon_use_ivar': (False, 'env'), 246 | 'napoleon_use_param': (True, 'env'), 247 | 'napoleon_use_rtype': (True, 'env'), 248 | 'napoleon_use_keyword': (True, 'env') 249 | } 250 | 251 | def __init__(self, **settings): 252 | # type: (Any) -> None 253 | for name, (default, rebuild) in self._config_values.items(): 254 | setattr(self, name, default) 255 | for name, value in settings.items(): 256 | setattr(self, name, value) 257 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | Defining commands 5 | ----------------- 6 | 7 | A command is a function decorated with ``@command``. mando tries to extract as 8 | much as information as possible from the function's docstring and its 9 | signature. 10 | 11 | The paragraph of the docstring is the command's **help**. For optimal results 12 | it shouldn't be longer than one line. The second paragraph contains the 13 | command's **description**, which can be as long as needed. If only one 14 | paragraph is present, it is used for both the help and the description. 15 | You can document the parameters with the common Sphinx's ``:param::`` syntax. 16 | 17 | For example, this program generates the following helps:: 18 | 19 | from mando import command, main 20 | 21 | 22 | @command 23 | def cmd(foo, bar): 24 | '''Here stands the help. 25 | 26 | And here the description of this useless command. 27 | 28 | :param foo: Well, the first arg. 29 | :param bar: Obviously the second arg. Nonsense.''' 30 | 31 | print(arg, bar) 32 | 33 | 34 | if __name__ == '__main__': 35 | main() 36 | 37 | .. code-block:: console 38 | 39 | $ python command.py -h 40 | usage: command.py [-h] {cmd} ... 41 | 42 | positional arguments: 43 | {cmd} 44 | cmd Here stands the help. 45 | 46 | optional arguments: 47 | -h, --help show this help message and exit 48 | $ python command.py cmd -h 49 | usage: command.py cmd [-h] foo bar 50 | 51 | And here the description of this useless command. 52 | 53 | positional arguments: 54 | foo Well, the first arg. 55 | bar Obviously the second arg. Nonsense. 56 | 57 | optional arguments: 58 | -h, --help show this help message and exit 59 | 60 | 61 | Long and short options (flags) 62 | ------------------------------ 63 | 64 | You can specify short options in the docstring as well, with the ``:param:`` 65 | syntax. The recognized formats are these: 66 | 67 | * ``:param -O: Option help`` 68 | * ``:param --option: Option help`` 69 | * ``:param -o, --output: Option help`` 70 | 71 | Example:: 72 | 73 | from mando import command, main 74 | 75 | 76 | @command 77 | def ex(foo, b=None, spam=None): 78 | '''Nothing interesting. 79 | 80 | :param foo: Bla bla. 81 | :param -b: A little flag. 82 | :param -s, --spam: Spam spam spam spam.''' 83 | 84 | print(foo, b, spam) 85 | 86 | if __name__ == '__main__': 87 | main() 88 | 89 | Usage: 90 | 91 | .. code-block:: console 92 | 93 | $ python short_options.py ex -h 94 | usage: short_options.py ex [-h] [-b B] [-s SPAM] foo 95 | 96 | Nothing interesting. 97 | 98 | positional arguments: 99 | foo Bla bla. 100 | 101 | optional arguments: 102 | -h, --help show this help message and exit 103 | -b B A little flag. 104 | -s SPAM, --spam SPAM Spam spam spam spam. 105 | $ python short_options.py ex 2 106 | ('2', None, None) 107 | $ python short_options.py ex 2 -b 8 108 | ('2', '8', None) 109 | $ python short_options.py ex 2 -b 8 -s 9 110 | ('2', '8', '9') 111 | $ python short_options.py ex 2 -b 8 --spam 9 112 | ('2', '8', '9') 113 | 114 | 115 | How default arguments are handled 116 | --------------------------------- 117 | 118 | If an argument has a default, then mando takes it as an optional argument, 119 | while those which do not have a default are interpreted as positional 120 | arguments. Here are the actions taken by mando when a default argument is 121 | encountered: 122 | 123 | +------------------------+-----------------------------------------------------+ 124 | | Default argument type | What mando specifies in ``add_argument()`` | 125 | +========================+=====================================================+ 126 | | bool | *action* ``store_true`` or ``store_false`` is added | 127 | +------------------------+-----------------------------------------------------+ 128 | | list | *action* ``append`` is added. | 129 | +------------------------+-----------------------------------------------------+ 130 | | int | *type* ``int()`` is added. | 131 | +------------------------+-----------------------------------------------------+ 132 | | float | *type* ``float()`` is added. | 133 | +------------------------+-----------------------------------------------------+ 134 | | str | *type* ``str()`` is added. | 135 | +------------------------+-----------------------------------------------------+ 136 | 137 | So, for example, if a default argument is an integer, mando will automatically 138 | convert command line arguments to ``int()``:: 139 | 140 | from mando import command, main 141 | 142 | 143 | @command 144 | def po(a=2, b=3): 145 | print(a ** b) 146 | 147 | 148 | if __name__ == '__main__': 149 | main() 150 | 151 | .. code-block:: console 152 | 153 | $ python default_args.py po -h 154 | usage: default_args.py po [-h] [-a A] [-b B] 155 | 156 | optional arguments: 157 | -h, --help show this help message and exit 158 | -a A 159 | -b B 160 | $ python default_args.py po -a 4 -b 9 161 | 262144 162 | 163 | Note that passing the arguments positionally does not work, because ``argparse`` 164 | expects optional args and ``a`` and ``b`` are already filled with defaults: 165 | 166 | .. code-block:: console 167 | 168 | $ python default_args.py po 169 | 8 170 | $ python default_args.py po 9 8 171 | usage: default_args.py [-h] {po} ... 172 | default_args.py: error: unrecognized arguments: 9 8 173 | 174 | To overcome this, mando allows you to specify positional arguments' types in 175 | the docstring, as explained in the next section. 176 | 177 | 178 | Adding *type* and *metavar* in the docstring 179 | -------------------------------------------- 180 | 181 | This is especially useful for positional arguments, but it is usually used for 182 | all type of arguments. 183 | The notation is this: ``:param {opt-name} : Help``. ```` must be a 184 | built-in type among the following: 185 | 186 | * ````, ````, ```` to cast to ``int()``; 187 | * also ````, ````, ```` to cast to ``int()``; 188 | * ````, ````, ```` to cast to ``str()``; 189 | * ````, ```` to cast to ``float()``. 190 | 191 | mando also adds ```` as a metavar. 192 | Actual usage:: 193 | 194 | from mando import command, main 195 | 196 | 197 | @command 198 | def pow(a, b, mod=None): 199 | '''Mimic Python's pow() function. 200 | 201 | :param a : The base. 202 | :param b : The exponent. 203 | :param -m, --mod : Modulus.''' 204 | 205 | if mod is not None: 206 | print((a ** b) % mod) 207 | else: 208 | print(a ** b) 209 | 210 | 211 | if __name__ == '__main__': 212 | main() 213 | 214 | .. code-block:: console 215 | 216 | $ python types.py pow -h 217 | usage: types.py pow [-h] [-m ] a b 218 | 219 | Mimic Python's pow() function. 220 | 221 | positional arguments: 222 | a The base. 223 | b The exponent. 224 | 225 | optional arguments: 226 | -h, --help show this help message and exit 227 | -m , --mod 228 | Modulus. 229 | $ python types.py pow 5 8 230 | 390625.0 231 | $ python types.py pow 4.5 8.3 232 | 264036.437449 233 | $ python types.py pow 5 8 -m 8 234 | 1.0 235 | 236 | Adding *type* in the signature 237 | ------------------------------ 238 | 239 | If running Python 3, mando can use type annotations to convert argument types. 240 | Since type annotations can be any callable, this allows more flexibility than 241 | the hard-coded list of types permitted by the docstring method:: 242 | 243 | from mando import command, main 244 | 245 | # Note: don't actually do this. 246 | def double_int(n): 247 | return int(n) * 2 248 | 249 | 250 | @command 251 | def dup(string, times: double_int): 252 | """ 253 | Duplicate text. 254 | 255 | :param string: The text to duplicate. 256 | :param times: How many times to duplicate. 257 | """ 258 | print(string * times) 259 | 260 | 261 | if __name__ == "__main__": 262 | main() 263 | 264 | .. code-block:: console 265 | 266 | $ python3 test.py dup "test " 2 267 | test test test test 268 | $ python3 test.py dup "test " foo 269 | usage: test.py dup [-h] string times 270 | test.py dup: error: argument times: invalid double_int value: 'foo' 271 | 272 | 273 | Overriding arguments with ``@arg`` 274 | ---------------------------------- 275 | 276 | You may need to specify some argument to argparse, and it is not possible to 277 | include in the docstring. mando provides the ``@arg`` decorator to accomplish 278 | this. Its signature is as follows: ``@arg(arg_name, *args, **kwargs)``, where 279 | ``arg_name`` must be among the function's arguments, while the remaining 280 | arguments will be directly passed to ``argparse.add_argument()``. 281 | Note that this decorator will override other arguments that mando inferred 282 | either from the defaults or from the docstring. 283 | 284 | ``@command`` Arguments 285 | ---------------------- 286 | 287 | There are three special arguments to the ``@command()`` decorator to allow for 288 | special processing for the decorated function. The first argument, also 289 | available as keyword ``name='alias_name'`` will allow for an alias of the 290 | command. The second argument, also available as keyword ``doctype='rest'`` 291 | allows for Numpy or Google formatted docstrings to be used. The third is only 292 | available as keyword ``formatter_class='argparse_formatter_class'`` to format 293 | the display of the docstring. 294 | 295 | Aliasing Commands 296 | ~~~~~~~~~~~~~~~~~ 297 | 298 | A common use-case for this is represented by a function with underscores in it. 299 | Usually commands have dashes instead. So, you may specify the aliasing name to 300 | the ``@command()`` decorator, this way:: 301 | 302 | @command('very-powerful-cmd') 303 | def very_powerful_cmd(arg, verbose=False): 304 | pass 305 | 306 | And call it as follows: 307 | 308 | .. code-block:: console 309 | 310 | $ python prog.py very-powerful-cmd 2 --verbose 311 | 312 | Note that the original name will be discarded and won't be usable. 313 | 314 | 315 | .. _docstring-style: 316 | 317 | Other Docstring Formats 318 | ~~~~~~~~~~~~~~~~~~~~~~~ 319 | 320 | There are three commonly accepted formats for docstrings. The Sphinx docstring, 321 | and the mando dialect of Sphinx described in this documentation are treated 322 | equally and is the default documentation style named ``rest`` for REStructured 323 | Text. The other two available styles are ``numpy`` and ``google``. This allows 324 | projects that use mando, but already have docstrings in these other formats not 325 | to have to convert the docstrings. 326 | 327 | An example of using a Numpy formatted docstring in mando:: 328 | 329 | @command(doctype='numpy') 330 | def simple_numpy_docstring(arg1, arg2="string"): 331 | '''One line summary. 332 | 333 | Extended description. 334 | 335 | Parameters 336 | ---------- 337 | arg1 : int 338 | Description of `arg1` 339 | arg2 : str 340 | Description of `arg2` 341 | 342 | Returns 343 | ------- 344 | str 345 | Description of return value. 346 | ''' 347 | return int(arg1) * arg2 348 | 349 | An example of using a Google formatted docstring in mando:: 350 | 351 | @program.command(doctype='google') 352 | def simple_google_docstring(arg1, arg2="string"): 353 | '''One line summary. 354 | 355 | Extended description. 356 | 357 | Args: 358 | arg1(int): Description of `arg1` 359 | arg2(str): Description of `arg2` 360 | Returns: 361 | str: Description of return value. 362 | ''' 363 | return int(arg1) * arg2 364 | 365 | 366 | Formatter Class 367 | ~~~~~~~~~~~~~~~ 368 | 369 | For the help display there is the opportunity to use special formatters. Any 370 | argparse compatible formatter class can be used. There is an alternative 371 | formatter class available with mando that will display on ANSI terminals. 372 | 373 | The ANSI formatter class has to be imported from mando and used as follows:: 374 | 375 | from mando.rst_text_formatter import RSTHelpFormatter 376 | 377 | @command(formatter_class=RSTHelpFormatter) 378 | def pow(a, b, mod=None): 379 | '''Mimic Python's pow() function. 380 | 381 | :param a : The base. 382 | :param b : The exponent. 383 | :param -m, --mod : Modulus.''' 384 | 385 | if mod is not None: 386 | print((a ** b) % mod) 387 | else: 388 | print(a ** b) 389 | 390 | 391 | Shell autocompletion 392 | -------------------- 393 | 394 | Mando supports autocompletion via the optional dependency ``argcomplete``. If 395 | that package is installed, mando detects it automatically without the need to 396 | do anything else. 397 | -------------------------------------------------------------------------------- /mando/napoleon/docstring.py: -------------------------------------------------------------------------------- 1 | """ 2 | sphinx.ext.napoleon.docstring 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | 6 | Classes for docstring parsing and formatting. 7 | 8 | 9 | :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. 10 | :license: BSD, see LICENSE for details. 11 | """ 12 | 13 | from collections.abc import Callable 14 | import inspect 15 | import re 16 | 17 | from mando.napoleon.iterators import modify_iter 18 | from mando.napoleon.pycompat import UnicodeMixin 19 | 20 | 21 | _directive_regex = re.compile(r'\.\. \S+::') 22 | _google_section_regex = re.compile(r'^(\s|\w)+:\s*$') 23 | _google_typed_arg_regex = re.compile(r'\s*(.+?)\s*\(\s*(.*[^\s]+)\s*\)') 24 | _numpy_section_regex = re.compile(r'^[=\-`:\'"~^_*+#<>]{2,}\s*$') 25 | _single_colon_regex = re.compile(r'(?\()?' 30 | r'(\d+|#|[ivxlcdm]+|[IVXLCDM]+|[a-zA-Z])' 31 | r'(?(paren)\)|\.)(\s+\S|\s*$)') 32 | 33 | 34 | class GoogleDocstring(UnicodeMixin): 35 | """Convert Google style docstrings to reStructuredText. 36 | 37 | Parameters 38 | ---------- 39 | docstring : :obj:`str` or :obj:`list` of :obj:`str` 40 | The docstring to parse, given either as a string or split into 41 | individual lines. 42 | config: :obj:`sphinx.ext.napoleon.Config` or :obj:`sphinx.config.Config` 43 | The configuration settings to use. If not given, defaults to the 44 | config object on `app`; or if `app` is not given defaults to the 45 | a new :class:`sphinx.ext.napoleon.Config` object. 46 | 47 | 48 | Other Parameters 49 | ---------------- 50 | app : :class:`sphinx.application.Sphinx`, optional 51 | Application object representing the Sphinx process. 52 | what : :obj:`str`, optional 53 | A string specifying the type of the object to which the docstring 54 | belongs. Valid values: "module", "class", "exception", "function", 55 | "method", "attribute". 56 | name : :obj:`str`, optional 57 | The fully qualified name of the object. 58 | obj : module, class, exception, function, method, or attribute 59 | The object to which the docstring belongs. 60 | options : :class:`sphinx.ext.autodoc.Options`, optional 61 | The options given to the directive: an object with attributes 62 | inherited_members, undoc_members, show_inheritance and noindex that 63 | are True if the flag option of same name was given to the auto 64 | directive. 65 | 66 | 67 | Example 68 | ------- 69 | >>> from sphinx.ext.napoleon import Config 70 | >>> config = Config(napoleon_use_param=True, napoleon_use_rtype=True) 71 | >>> docstring = '''One line summary. 72 | ... 73 | ... Extended description. 74 | ... 75 | ... Args: 76 | ... arg1(int): Description of `arg1` 77 | ... arg2(str): Description of `arg2` 78 | ... Returns: 79 | ... str: Description of return value. 80 | ... ''' 81 | >>> print(GoogleDocstring(docstring, config)) 82 | One line summary. 83 | 84 | Extended description. 85 | 86 | :param arg1: Description of `arg1` 87 | :type arg1: int 88 | :param arg2: Description of `arg2` 89 | :type arg2: str 90 | 91 | :returns: Description of return value. 92 | :rtype: str 93 | 94 | 95 | """ 96 | def __init__(self, docstring, config=None, app=None, what='', name='', 97 | obj=None, options=None): 98 | # type: (Union[unicode, List[unicode]], SphinxConfig, Sphinx, unicode, unicode, Any, Any) -> None # NOQA 99 | self._config = config 100 | self._app = app 101 | 102 | if not self._config: 103 | from sphinx.ext.napoleon import Config 104 | self._config = self._app and self._app.config or Config() # type: ignore 105 | 106 | if not what: 107 | if inspect.isclass(obj): 108 | what = 'class' 109 | elif inspect.ismodule(obj): 110 | what = 'module' 111 | elif isinstance(obj, Callable): # type: ignore 112 | what = 'function' 113 | else: 114 | what = 'object' 115 | 116 | self._what = what 117 | self._name = name 118 | self._obj = obj 119 | self._opt = options 120 | if isinstance(docstring, str): 121 | docstring = docstring.splitlines() 122 | self._lines = docstring 123 | self._line_iter = modify_iter(docstring, modifier=lambda s: s.rstrip()) 124 | self._parsed_lines = [] # type: List[unicode] 125 | self._is_in_section = False 126 | self._section_indent = 0 127 | if not hasattr(self, '_directive_sections'): 128 | self._directive_sections = [] # type: List[unicode] 129 | if not hasattr(self, '_sections'): 130 | self._sections = { 131 | 'args': self._parse_parameters_section, 132 | 'arguments': self._parse_parameters_section, 133 | 'attributes': self._parse_attributes_section, 134 | 'example': self._parse_examples_section, 135 | 'examples': self._parse_examples_section, 136 | 'keyword args': self._parse_keyword_arguments_section, 137 | 'keyword arguments': self._parse_keyword_arguments_section, 138 | 'methods': self._parse_methods_section, 139 | 'note': self._parse_note_section, 140 | 'notes': self._parse_notes_section, 141 | 'other parameters': self._parse_other_parameters_section, 142 | 'parameters': self._parse_parameters_section, 143 | 'return': self._parse_returns_section, 144 | 'returns': self._parse_returns_section, 145 | 'raises': self._parse_raises_section, 146 | 'references': self._parse_references_section, 147 | 'see also': self._parse_see_also_section, 148 | 'todo': self._parse_todo_section, 149 | 'warning': self._parse_warning_section, 150 | 'warnings': self._parse_warning_section, 151 | 'warns': self._parse_warns_section, 152 | 'yield': self._parse_yields_section, 153 | 'yields': self._parse_yields_section, 154 | } # type: Dict[unicode, Callable] 155 | self._parse() 156 | 157 | def __unicode__(self): 158 | # type: () -> unicode 159 | """Return the parsed docstring in reStructuredText format. 160 | 161 | Returns 162 | ------- 163 | unicode 164 | Unicode version of the docstring. 165 | 166 | """ 167 | return '\n'.join(self.lines()) 168 | 169 | def lines(self): 170 | # type: () -> List[unicode] 171 | """Return the parsed lines of the docstring in reStructuredText format. 172 | 173 | Returns 174 | ------- 175 | list(str) 176 | The lines of the docstring in a list. 177 | 178 | """ 179 | return self._parsed_lines 180 | 181 | def _consume_indented_block(self, indent=1): 182 | # type: (int) -> List[unicode] 183 | lines = [] 184 | line = self._line_iter.peek() 185 | while(not self._is_section_break() and 186 | (not line or self._is_indented(line, indent))): 187 | lines.append(next(self._line_iter)) # type: ignore 188 | line = self._line_iter.peek() 189 | return lines 190 | 191 | def _consume_contiguous(self): 192 | # type: () -> List[unicode] 193 | lines = [] 194 | while (self._line_iter.has_next() and 195 | self._line_iter.peek() and 196 | not self._is_section_header()): 197 | lines.append(next(self._line_iter)) # type: ignore 198 | return lines 199 | 200 | def _consume_empty(self): 201 | # type: () -> List[unicode] 202 | lines = [] 203 | line = self._line_iter.peek() 204 | while self._line_iter.has_next() and not line: 205 | lines.append(next(self._line_iter)) # type: ignore 206 | line = self._line_iter.peek() 207 | return lines 208 | 209 | def _consume_field(self, parse_type=True, prefer_type=False): 210 | # type: (bool, bool) -> Tuple[unicode, unicode, List[unicode]] 211 | line = next(self._line_iter) # type: ignore 212 | 213 | before, colon, after = self._partition_field_on_colon(line) 214 | _name, _type, _desc = before, '', after # type: unicode, unicode, unicode 215 | 216 | if parse_type: 217 | match = _google_typed_arg_regex.match(before) # type: ignore 218 | if match: 219 | _name = match.group(1) 220 | _type = match.group(2) 221 | 222 | _name = self._escape_args_and_kwargs(_name) 223 | 224 | if prefer_type and not _type: 225 | _type, _name = _name, _type 226 | indent = self._get_indent(line) + 1 227 | _descs = [_desc] + self._dedent(self._consume_indented_block(indent)) 228 | _descs = self.__class__(_descs, self._config).lines() 229 | return _name, _type, _descs 230 | 231 | def _consume_fields(self, parse_type=True, prefer_type=False): 232 | # type: (bool, bool) -> List[Tuple[unicode, unicode, List[unicode]]] 233 | self._consume_empty() 234 | fields = [] 235 | while not self._is_section_break(): 236 | _name, _type, _desc = self._consume_field(parse_type, prefer_type) 237 | if _name or _type or _desc: 238 | fields.append((_name, _type, _desc,)) 239 | return fields 240 | 241 | def _consume_inline_attribute(self): 242 | # type: () -> Tuple[unicode, List[unicode]] 243 | line = next(self._line_iter) # type: ignore 244 | _type, colon, _desc = self._partition_field_on_colon(line) 245 | if not colon: 246 | _type, _desc = _desc, _type 247 | _descs = [_desc] + self._dedent(self._consume_to_end()) 248 | _descs = self.__class__(_descs, self._config).lines() 249 | return _type, _descs 250 | 251 | def _consume_returns_section(self): 252 | # type: () -> List[Tuple[unicode, unicode, List[unicode]]] 253 | lines = self._dedent(self._consume_to_next_section()) 254 | if lines: 255 | before, colon, after = self._partition_field_on_colon(lines[0]) 256 | _name, _type, _desc = '', '', lines # type: unicode, unicode, List[unicode] 257 | 258 | if colon: 259 | if after: 260 | _desc = [after] + lines[1:] 261 | else: 262 | _desc = lines[1:] 263 | 264 | _type = before 265 | 266 | _desc = self.__class__(_desc, self._config).lines() 267 | return [(_name, _type, _desc,)] 268 | else: 269 | return [] 270 | 271 | def _consume_usage_section(self): 272 | # type: () -> List[unicode] 273 | lines = self._dedent(self._consume_to_next_section()) 274 | return lines 275 | 276 | def _consume_section_header(self): 277 | # type: () -> unicode 278 | section = next(self._line_iter) # type: ignore 279 | stripped_section = section.strip(':') 280 | if stripped_section.lower() in self._sections: 281 | section = stripped_section 282 | return section 283 | 284 | def _consume_to_end(self): 285 | # type: () -> List[unicode] 286 | lines = [] 287 | while self._line_iter.has_next(): 288 | lines.append(next(self._line_iter)) # type: ignore 289 | return lines 290 | 291 | def _consume_to_next_section(self): 292 | # type: () -> List[unicode] 293 | self._consume_empty() 294 | lines = [] 295 | while not self._is_section_break(): 296 | lines.append(next(self._line_iter)) # type: ignore 297 | return lines + self._consume_empty() 298 | 299 | def _dedent(self, lines, full=False): 300 | # type: (List[unicode], bool) -> List[unicode] 301 | if full: 302 | return [line.lstrip() for line in lines] 303 | else: 304 | min_indent = self._get_min_indent(lines) 305 | return [line[min_indent:] for line in lines] 306 | 307 | def _escape_args_and_kwargs(self, name): 308 | # type: (unicode) -> unicode 309 | if name[:2] == '**': 310 | return r'\*\*' + name[2:] 311 | elif name[:1] == '*': 312 | return r'\*' + name[1:] 313 | else: 314 | return name 315 | 316 | def _fix_field_desc(self, desc): 317 | # type: (List[unicode]) -> List[unicode] 318 | if self._is_list(desc): 319 | desc = [''] + desc 320 | elif desc[0].endswith('::'): 321 | desc_block = desc[1:] 322 | indent = self._get_indent(desc[0]) 323 | block_indent = self._get_initial_indent(desc_block) 324 | if block_indent > indent: 325 | desc = [''] + desc 326 | else: 327 | desc = ['', desc[0]] + self._indent(desc_block, 4) 328 | return desc 329 | 330 | def _format_admonition(self, admonition, lines): 331 | # type: (unicode, List[unicode]) -> List[unicode] 332 | lines = self._strip_empty(lines) 333 | if len(lines) == 1: 334 | return ['.. %s:: %s' % (admonition, lines[0].strip()), ''] 335 | elif lines: 336 | lines = self._indent(self._dedent(lines), 3) 337 | return ['.. %s::' % admonition, ''] + lines + [''] 338 | else: 339 | return ['.. %s::' % admonition, ''] 340 | 341 | def _format_block(self, prefix, lines, padding=None): 342 | # type: (unicode, List[unicode], unicode) -> List[unicode] 343 | if lines: 344 | if padding is None: 345 | padding = ' ' * len(prefix) 346 | result_lines = [] 347 | for i, line in enumerate(lines): 348 | if i == 0: 349 | result_lines.append((prefix + line).rstrip()) 350 | elif line: 351 | result_lines.append(padding + line) 352 | else: 353 | result_lines.append('') 354 | return result_lines 355 | else: 356 | return [prefix] 357 | 358 | def _format_docutils_params(self, fields, field_role='param', 359 | type_role='type'): 360 | # type: (List[Tuple[unicode, unicode, List[unicode]]], unicode, unicode) -> List[unicode] # NOQA 361 | lines = [] 362 | for _name, _type, _desc in fields: 363 | _desc = self._strip_empty(_desc) 364 | if any(_desc): 365 | _desc = self._fix_field_desc(_desc) 366 | field = ':%s %s: ' % (field_role, _name) 367 | lines.extend(self._format_block(field, _desc)) 368 | else: 369 | lines.append(':%s %s:' % (field_role, _name)) 370 | 371 | if _type: 372 | lines.append(':%s %s: %s' % (type_role, _name, _type)) 373 | return lines + [''] 374 | 375 | def _format_field(self, _name, _type, _desc): 376 | # type: (unicode, unicode, List[unicode]) -> List[unicode] 377 | _desc = self._strip_empty(_desc) 378 | has_desc = any(_desc) 379 | separator = has_desc and ' -- ' or '' 380 | if _name: 381 | if _type: 382 | if '`' in _type: 383 | field = '**%s** (%s)%s' % (_name, _type, separator) # type: unicode 384 | else: 385 | field = '**%s** (*%s*)%s' % (_name, _type, separator) 386 | else: 387 | field = '**%s**%s' % (_name, separator) 388 | elif _type: 389 | if '`' in _type: 390 | field = '%s%s' % (_type, separator) 391 | else: 392 | field = '*%s*%s' % (_type, separator) 393 | else: 394 | field = '' 395 | 396 | if has_desc: 397 | _desc = self._fix_field_desc(_desc) 398 | if _desc[0]: 399 | return [field + _desc[0]] + _desc[1:] 400 | else: 401 | return [field] + _desc 402 | else: 403 | return [field] 404 | 405 | def _format_fields(self, field_type, fields): 406 | # type: (unicode, List[Tuple[unicode, unicode, List[unicode]]]) -> List[unicode] 407 | field_type = ':%s:' % field_type.strip() 408 | padding = ' ' * len(field_type) 409 | multi = len(fields) > 1 410 | lines = [] # type: List[unicode] 411 | for _name, _type, _desc in fields: 412 | field = self._format_field(_name, _type, _desc) 413 | if multi: 414 | if lines: 415 | lines.extend(self._format_block(padding + ' * ', field)) 416 | else: 417 | lines.extend(self._format_block(field_type + ' * ', field)) 418 | else: 419 | lines.extend(self._format_block(field_type + ' ', field)) 420 | if lines and lines[-1]: 421 | lines.append('') 422 | return lines 423 | 424 | def _get_current_indent(self, peek_ahead=0): 425 | # type: (int) -> int 426 | line = self._line_iter.peek(peek_ahead + 1)[peek_ahead] 427 | while line != self._line_iter.sentinel: 428 | if line: 429 | return self._get_indent(line) 430 | peek_ahead += 1 431 | line = self._line_iter.peek(peek_ahead + 1)[peek_ahead] 432 | return 0 433 | 434 | def _get_indent(self, line): 435 | # type: (unicode) -> int 436 | for i, s in enumerate(line): 437 | if not s.isspace(): 438 | return i 439 | return len(line) 440 | 441 | def _get_initial_indent(self, lines): 442 | # type: (List[unicode]) -> int 443 | for line in lines: 444 | if line: 445 | return self._get_indent(line) 446 | return 0 447 | 448 | def _get_min_indent(self, lines): 449 | # type: (List[unicode]) -> int 450 | min_indent = None 451 | for line in lines: 452 | if line: 453 | indent = self._get_indent(line) 454 | if min_indent is None: 455 | min_indent = indent 456 | elif indent < min_indent: 457 | min_indent = indent 458 | return min_indent or 0 459 | 460 | def _indent(self, lines, n=4): 461 | # type: (List[unicode], int) -> List[unicode] 462 | return [(' ' * n) + line for line in lines] 463 | 464 | def _is_indented(self, line, indent=1): 465 | # type: (unicode, int) -> bool 466 | for i, s in enumerate(line): 467 | if i >= indent: 468 | return True 469 | elif not s.isspace(): 470 | return False 471 | return False 472 | 473 | def _is_list(self, lines): 474 | # type: (List[unicode]) -> bool 475 | if not lines: 476 | return False 477 | if _bullet_list_regex.match(lines[0]): # type: ignore 478 | return True 479 | if _enumerated_list_regex.match(lines[0]): # type: ignore 480 | return True 481 | if len(lines) < 2 or lines[0].endswith('::'): 482 | return False 483 | indent = self._get_indent(lines[0]) 484 | next_indent = indent 485 | for line in lines[1:]: 486 | if line: 487 | next_indent = self._get_indent(line) 488 | break 489 | return next_indent > indent 490 | 491 | def _is_section_header(self): 492 | # type: () -> bool 493 | section = self._line_iter.peek().lower() 494 | match = _google_section_regex.match(section) 495 | if match and section.strip(':') in self._sections: 496 | header_indent = self._get_indent(section) 497 | section_indent = self._get_current_indent(peek_ahead=1) 498 | return section_indent > header_indent 499 | elif self._directive_sections: 500 | if _directive_regex.match(section): 501 | for directive_section in self._directive_sections: 502 | if section.startswith(directive_section): 503 | return True 504 | return False 505 | 506 | def _is_section_break(self): 507 | # type: () -> bool 508 | line = self._line_iter.peek() 509 | return (not self._line_iter.has_next() or 510 | self._is_section_header() or 511 | (self._is_in_section and 512 | line and 513 | not self._is_indented(line, self._section_indent))) 514 | 515 | def _parse(self): 516 | # type: () -> None 517 | self._parsed_lines = self._consume_empty() 518 | 519 | if self._name and (self._what == 'attribute' or self._what == 'data'): 520 | self._parsed_lines.extend(self._parse_attribute_docstring()) 521 | return 522 | 523 | while self._line_iter.has_next(): 524 | if self._is_section_header(): 525 | try: 526 | section = self._consume_section_header() 527 | self._is_in_section = True 528 | self._section_indent = self._get_current_indent() 529 | if _directive_regex.match(section): # type: ignore 530 | lines = [section] + self._consume_to_next_section() 531 | else: 532 | lines = self._sections[section.lower()](section) 533 | finally: 534 | self._is_in_section = False 535 | self._section_indent = 0 536 | else: 537 | if not self._parsed_lines: 538 | lines = self._consume_contiguous() + self._consume_empty() 539 | else: 540 | lines = self._consume_to_next_section() 541 | self._parsed_lines.extend(lines) 542 | 543 | def _parse_attribute_docstring(self): 544 | # type: () -> List[unicode] 545 | _type, _desc = self._consume_inline_attribute() 546 | return self._format_field('', _type, _desc) 547 | 548 | def _parse_attributes_section(self, section): 549 | # type: (unicode) -> List[unicode] 550 | lines = [] 551 | for _name, _type, _desc in self._consume_fields(): 552 | if self._config.napoleon_use_ivar: 553 | field = ':ivar %s: ' % _name # type: unicode 554 | lines.extend(self._format_block(field, _desc)) 555 | if _type: 556 | lines.append(':vartype %s: %s' % (_name, _type)) 557 | else: 558 | lines.extend(['.. attribute:: ' + _name, '']) 559 | fields = self._format_field('', _type, _desc) 560 | lines.extend(self._indent(fields, 3)) 561 | lines.append('') 562 | if self._config.napoleon_use_ivar: 563 | lines.append('') 564 | return lines 565 | 566 | def _parse_examples_section(self, section): 567 | # type: (unicode) -> List[unicode] 568 | use_admonition = self._config.napoleon_use_admonition_for_examples 569 | return self._parse_generic_section(section, use_admonition) 570 | 571 | def _parse_usage_section(self, section): 572 | # type: (unicode) -> List[unicode] 573 | header = ['.. rubric:: Usage:', ''] # type: List[unicode] 574 | block = ['.. code-block:: python', ''] # type: List[unicode] 575 | lines = self._consume_usage_section() 576 | lines = self._indent(lines, 3) 577 | return header + block + lines + [''] 578 | 579 | def _parse_generic_section(self, section, use_admonition): 580 | # type: (unicode, bool) -> List[unicode] 581 | lines = self._strip_empty(self._consume_to_next_section()) 582 | lines = self._dedent(lines) 583 | if use_admonition: 584 | header = '.. admonition:: %s' % section # type: unicode 585 | lines = self._indent(lines, 3) 586 | else: 587 | header = '.. rubric:: %s' % section 588 | if lines: 589 | return [header, ''] + lines + [''] 590 | else: 591 | return [header, ''] 592 | 593 | def _parse_keyword_arguments_section(self, section): 594 | # type: (unicode) -> List[unicode] 595 | fields = self._consume_fields() 596 | if self._config.napoleon_use_keyword: 597 | return self._format_docutils_params( 598 | fields, 599 | field_role="keyword", 600 | type_role="kwtype") 601 | else: 602 | return self._format_fields('Keyword Arguments', fields) 603 | 604 | def _parse_methods_section(self, section): 605 | # type: (unicode) -> List[unicode] 606 | lines = [] # type: List[unicode] 607 | for _name, _, _desc in self._consume_fields(parse_type=False): 608 | lines.append('.. method:: %s' % _name) 609 | if _desc: 610 | lines.extend([''] + self._indent(_desc, 3)) 611 | lines.append('') 612 | return lines 613 | 614 | def _parse_note_section(self, section): 615 | # type: (unicode) -> List[unicode] 616 | lines = self._consume_to_next_section() 617 | return self._format_admonition('note', lines) 618 | 619 | def _parse_notes_section(self, section): 620 | # type: (unicode) -> List[unicode] 621 | use_admonition = self._config.napoleon_use_admonition_for_notes 622 | return self._parse_generic_section('Notes', use_admonition) 623 | 624 | def _parse_other_parameters_section(self, section): 625 | # type: (unicode) -> List[unicode] 626 | return self._format_fields('Other Parameters', self._consume_fields()) 627 | 628 | def _parse_parameters_section(self, section): 629 | # type: (unicode) -> List[unicode] 630 | fields = self._consume_fields() 631 | if self._config.napoleon_use_param: 632 | return self._format_docutils_params(fields) 633 | else: 634 | return self._format_fields('Parameters', fields) 635 | 636 | def _parse_raises_section(self, section): 637 | # type: (unicode) -> List[unicode] 638 | fields = self._consume_fields(parse_type=False, prefer_type=True) 639 | field_type = ':raises:' 640 | padding = ' ' * len(field_type) 641 | multi = len(fields) > 1 642 | lines = [] # type: List[unicode] 643 | for _, _type, _desc in fields: 644 | _desc = self._strip_empty(_desc) 645 | has_desc = any(_desc) 646 | separator = has_desc and ' -- ' or '' 647 | if _type: 648 | has_refs = '`' in _type or ':' in _type 649 | has_space = any(c in ' \t\n\v\f ' for c in _type) 650 | 651 | if not has_refs and not has_space: 652 | _type = ':exc:`%s`%s' % (_type, separator) 653 | elif has_desc and has_space: 654 | _type = '*%s*%s' % (_type, separator) 655 | else: 656 | _type = '%s%s' % (_type, separator) 657 | 658 | if has_desc: 659 | field = [_type + _desc[0]] + _desc[1:] 660 | else: 661 | field = [_type] 662 | else: 663 | field = _desc 664 | if multi: 665 | if lines: 666 | lines.extend(self._format_block(padding + ' * ', field)) 667 | else: 668 | lines.extend(self._format_block(field_type + ' * ', field)) 669 | else: 670 | lines.extend(self._format_block(field_type + ' ', field)) 671 | if lines and lines[-1]: 672 | lines.append('') 673 | return lines 674 | 675 | def _parse_references_section(self, section): 676 | # type: (unicode) -> List[unicode] 677 | use_admonition = self._config.napoleon_use_admonition_for_references 678 | return self._parse_generic_section('References', use_admonition) 679 | 680 | def _parse_returns_section(self, section): 681 | # type: (unicode) -> List[unicode] 682 | fields = self._consume_returns_section() 683 | multi = len(fields) > 1 684 | if multi: 685 | use_rtype = False 686 | else: 687 | use_rtype = self._config.napoleon_use_rtype 688 | 689 | lines = [] # type: List[unicode] 690 | for _name, _type, _desc in fields: 691 | if use_rtype: 692 | field = self._format_field(_name, '', _desc) 693 | else: 694 | field = self._format_field(_name, _type, _desc) 695 | 696 | if multi: 697 | if lines: 698 | lines.extend(self._format_block(' * ', field)) 699 | else: 700 | lines.extend(self._format_block(':returns: * ', field)) 701 | else: 702 | lines.extend(self._format_block(':returns: ', field)) 703 | if _type and use_rtype: 704 | lines.extend([':rtype: %s' % _type, '']) 705 | if lines and lines[-1]: 706 | lines.append('') 707 | return lines 708 | 709 | def _parse_see_also_section(self, section): 710 | # type: (unicode) -> List[unicode] 711 | lines = self._consume_to_next_section() 712 | return self._format_admonition('seealso', lines) 713 | 714 | def _parse_todo_section(self, section): 715 | # type: (unicode) -> List[unicode] 716 | lines = self._consume_to_next_section() 717 | return self._format_admonition('todo', lines) 718 | 719 | def _parse_warning_section(self, section): 720 | # type: (unicode) -> List[unicode] 721 | lines = self._consume_to_next_section() 722 | return self._format_admonition('warning', lines) 723 | 724 | def _parse_warns_section(self, section): 725 | # type: (unicode) -> List[unicode] 726 | return self._format_fields('Warns', self._consume_fields()) 727 | 728 | def _parse_yields_section(self, section): 729 | # type: (unicode) -> List[unicode] 730 | fields = self._consume_returns_section() 731 | return self._format_fields('Yields', fields) 732 | 733 | def _partition_field_on_colon(self, line): 734 | # type: (unicode) -> Tuple[unicode, unicode, unicode] 735 | before_colon = [] 736 | after_colon = [] 737 | colon = '' 738 | found_colon = False 739 | for i, source in enumerate(_xref_regex.split(line)): # type: ignore 740 | if found_colon: 741 | after_colon.append(source) 742 | else: 743 | m = _single_colon_regex.search(source) 744 | if (i % 2) == 0 and m: 745 | found_colon = True 746 | colon = source[m.start(): m.end()] 747 | before_colon.append(source[:m.start()]) 748 | after_colon.append(source[m.end():]) 749 | else: 750 | before_colon.append(source) 751 | 752 | return ("".join(before_colon).strip(), 753 | colon, 754 | "".join(after_colon).strip()) 755 | 756 | def _strip_empty(self, lines): 757 | # type: (List[unicode]) -> List[unicode] 758 | if lines: 759 | start = -1 760 | for i, line in enumerate(lines): 761 | if line: 762 | start = i 763 | break 764 | if start == -1: 765 | lines = [] 766 | end = -1 767 | for i in reversed(range(len(lines))): 768 | line = lines[i] 769 | if line: 770 | end = i 771 | break 772 | if start > 0 or end + 1 < len(lines): 773 | lines = lines[start:end + 1] 774 | return lines 775 | 776 | 777 | class NumpyDocstring(GoogleDocstring): 778 | """Convert NumPy style docstrings to reStructuredText. 779 | 780 | Parameters 781 | ---------- 782 | docstring : :obj:`str` or :obj:`list` of :obj:`str` 783 | The docstring to parse, given either as a string or split into 784 | individual lines. 785 | config: :obj:`sphinx.ext.napoleon.Config` or :obj:`sphinx.config.Config` 786 | The configuration settings to use. If not given, defaults to the 787 | config object on `app`; or if `app` is not given defaults to the 788 | a new :class:`sphinx.ext.napoleon.Config` object. 789 | 790 | 791 | Other Parameters 792 | ---------------- 793 | app : :class:`sphinx.application.Sphinx`, optional 794 | Application object representing the Sphinx process. 795 | what : :obj:`str`, optional 796 | A string specifying the type of the object to which the docstring 797 | belongs. Valid values: "module", "class", "exception", "function", 798 | "method", "attribute". 799 | name : :obj:`str`, optional 800 | The fully qualified name of the object. 801 | obj : module, class, exception, function, method, or attribute 802 | The object to which the docstring belongs. 803 | options : :class:`sphinx.ext.autodoc.Options`, optional 804 | The options given to the directive: an object with attributes 805 | inherited_members, undoc_members, show_inheritance and noindex that 806 | are True if the flag option of same name was given to the auto 807 | directive. 808 | 809 | 810 | Example 811 | ------- 812 | >>> from sphinx.ext.napoleon import Config 813 | >>> config = Config(napoleon_use_param=True, napoleon_use_rtype=True) 814 | >>> docstring = '''One line summary. 815 | ... 816 | ... Extended description. 817 | ... 818 | ... Parameters 819 | ... ---------- 820 | ... arg1 : int 821 | ... Description of `arg1` 822 | ... arg2 : str 823 | ... Description of `arg2` 824 | ... Returns 825 | ... ------- 826 | ... str 827 | ... Description of return value. 828 | ... ''' 829 | >>> print(NumpyDocstring(docstring, config)) 830 | One line summary. 831 | 832 | Extended description. 833 | 834 | :param arg1: Description of `arg1` 835 | :type arg1: int 836 | :param arg2: Description of `arg2` 837 | :type arg2: str 838 | 839 | :returns: Description of return value. 840 | :rtype: str 841 | 842 | 843 | Methods 844 | ------- 845 | __str__() 846 | Return the parsed docstring in reStructuredText format. 847 | 848 | Returns 849 | ------- 850 | str 851 | UTF-8 encoded version of the docstring. 852 | 853 | __unicode__() 854 | Return the parsed docstring in reStructuredText format. 855 | 856 | Returns 857 | ------- 858 | unicode 859 | Unicode version of the docstring. 860 | 861 | lines() 862 | Return the parsed lines of the docstring in reStructuredText format. 863 | 864 | Returns 865 | ------- 866 | list(str) 867 | The lines of the docstring in a list. 868 | 869 | """ 870 | def __init__(self, docstring, config=None, app=None, what='', name='', 871 | obj=None, options=None): 872 | # type: (Union[unicode, List[unicode]], SphinxConfig, Sphinx, unicode, unicode, Any, Any) -> None # NOQA 873 | self._directive_sections = ['.. index::'] 874 | super(NumpyDocstring, self).__init__(docstring, config, app, what, 875 | name, obj, options) 876 | 877 | def _consume_field(self, parse_type=True, prefer_type=False): 878 | # type: (bool, bool) -> Tuple[unicode, unicode, List[unicode]] 879 | line = next(self._line_iter) # type: ignore 880 | if parse_type: 881 | _name, _, _type = self._partition_field_on_colon(line) 882 | else: 883 | _name, _type = line, '' 884 | _name, _type = _name.strip(), _type.strip() 885 | _name = self._escape_args_and_kwargs(_name) 886 | 887 | if prefer_type and not _type: 888 | _type, _name = _name, _type 889 | indent = self._get_indent(line) + 1 890 | _desc = self._dedent(self._consume_indented_block(indent)) 891 | _desc = self.__class__(_desc, self._config).lines() 892 | return _name, _type, _desc 893 | 894 | def _consume_returns_section(self): 895 | # type: () -> List[Tuple[unicode, unicode, List[unicode]]] 896 | return self._consume_fields(prefer_type=True) 897 | 898 | def _consume_section_header(self): 899 | # type: () -> unicode 900 | section = next(self._line_iter) # type: ignore 901 | if not _directive_regex.match(section): 902 | # Consume the header underline 903 | next(self._line_iter) # type: ignore 904 | return section 905 | 906 | def _is_section_break(self): 907 | # type: () -> bool 908 | line1, line2 = self._line_iter.peek(2) 909 | return (not self._line_iter.has_next() or 910 | self._is_section_header() or 911 | ['', ''] == [line1, line2] or 912 | (self._is_in_section and 913 | line1 and 914 | not self._is_indented(line1, self._section_indent))) 915 | 916 | def _is_section_header(self): 917 | # type: () -> bool 918 | section, underline = self._line_iter.peek(2) 919 | section = section.lower() 920 | if section in self._sections and isinstance(underline, str): 921 | return bool(_numpy_section_regex.match(underline)) # type: ignore 922 | elif self._directive_sections: 923 | if _directive_regex.match(section): 924 | for directive_section in self._directive_sections: 925 | if section.startswith(directive_section): 926 | return True 927 | return False 928 | 929 | _name_rgx = re.compile(r"^\s*(:(?P\w+):`(?P[a-zA-Z0-9_.-]+)`|" 930 | r" (?P[a-zA-Z0-9_.-]+))\s*", re.X) 931 | 932 | def _parse_see_also_section(self, section): 933 | # type: (unicode) -> List[unicode] 934 | lines = self._consume_to_next_section() 935 | try: 936 | return self._parse_numpydoc_see_also_section(lines) 937 | except ValueError: 938 | return self._format_admonition('seealso', lines) 939 | 940 | def _parse_numpydoc_see_also_section(self, content): 941 | # type: (List[unicode]) -> List[unicode] 942 | """ 943 | Derived from the NumpyDoc implementation of _parse_see_also. 944 | 945 | See Also 946 | -------- 947 | func_name : Descriptive text 948 | continued text 949 | another_func_name : Descriptive text 950 | func_name1, func_name2, :meth:`func_name`, func_name3 951 | 952 | """ 953 | items = [] 954 | 955 | def parse_item_name(text): 956 | """Match ':role:`name`' or 'name'""" 957 | m = self._name_rgx.match(text) 958 | if m: 959 | g = m.groups() 960 | if g[1] is None: 961 | return g[3], None 962 | else: 963 | return g[2], g[1] 964 | raise ValueError("%s is not a item name" % text) 965 | 966 | def push_item(name, rest): 967 | if not name: 968 | return 969 | name, role = parse_item_name(name) 970 | items.append((name, list(rest), role)) 971 | del rest[:] 972 | 973 | current_func = None 974 | rest = [] # type: List[unicode] 975 | 976 | for line in content: 977 | if not line.strip(): 978 | continue 979 | 980 | m = self._name_rgx.match(line) # type: ignore 981 | if m and line[m.end():].strip().startswith(':'): 982 | push_item(current_func, rest) 983 | current_func, line = line[:m.end()], line[m.end():] 984 | rest = [line.split(':', 1)[1].strip()] 985 | if not rest[0]: 986 | rest = [] 987 | elif not line.startswith(' '): 988 | push_item(current_func, rest) 989 | current_func = None 990 | if ',' in line: 991 | for func in line.split(','): 992 | if func.strip(): 993 | push_item(func, []) 994 | elif line.strip(): 995 | current_func = line 996 | elif current_func is not None: 997 | rest.append(line.strip()) 998 | push_item(current_func, rest) 999 | 1000 | if not items: 1001 | return [] 1002 | 1003 | roles = { 1004 | 'method': 'meth', 1005 | 'meth': 'meth', 1006 | 'function': 'func', 1007 | 'func': 'func', 1008 | 'class': 'class', 1009 | 'exception': 'exc', 1010 | 'exc': 'exc', 1011 | 'object': 'obj', 1012 | 'obj': 'obj', 1013 | 'module': 'mod', 1014 | 'mod': 'mod', 1015 | 'data': 'data', 1016 | 'constant': 'const', 1017 | 'const': 'const', 1018 | 'attribute': 'attr', 1019 | 'attr': 'attr' 1020 | } # type: Dict[unicode, unicode] 1021 | if self._what is None: 1022 | func_role = 'obj' # type: unicode 1023 | else: 1024 | func_role = roles.get(self._what, '') 1025 | lines = [] # type: List[unicode] 1026 | last_had_desc = True 1027 | for func, desc, role in items: 1028 | if role: 1029 | link = ':%s:`%s`' % (role, func) 1030 | elif func_role: 1031 | link = ':%s:`%s`' % (func_role, func) 1032 | else: 1033 | link = "`%s`_" % func 1034 | if desc or last_had_desc: 1035 | lines += [''] 1036 | lines += [link] 1037 | else: 1038 | lines[-1] += ", %s" % link 1039 | if desc: 1040 | lines += self._indent([' '.join(desc)]) 1041 | last_had_desc = True 1042 | else: 1043 | last_had_desc = False 1044 | lines += [''] 1045 | 1046 | return self._format_admonition('seealso', lines) 1047 | --------------------------------------------------------------------------------