diff --git a/.gitignore b/.gitignore index 0c475f2..0c83979 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ target/ # PyCharm files .idea/ + +# Editor files +.*.swp diff --git a/.travis.yml b/.travis.yml index 144a699..bba5626 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,8 @@ matrix: allow_failures: - python: 3.7-dev install: - - pip install --upgrade setuptools + - pip install --upgrade pip setuptools coverage - pip install . script: - - python setup.py nosetests + - python -mcoverage run setup.py test + - python -mcoverage report diff --git a/defopt.py b/defopt.py index 65a56ae..f0d5d10 100644 --- a/defopt.py +++ b/defopt.py @@ -2,13 +2,15 @@ Run Python functions from the command line with ``run(func)``. """ -from __future__ import absolute_import, division, unicode_literals, print_function +from __future__ import ( + absolute_import, division, unicode_literals, print_function) import inspect import logging import re import sys -from argparse import SUPPRESS, ArgumentParser, RawTextHelpFormatter, ArgumentDefaultsHelpFormatter +from argparse import (SUPPRESS, ArgumentParser, RawTextHelpFormatter, + ArgumentDefaultsHelpFormatter) from collections import defaultdict, namedtuple, OrderedDict from enum import Enum from typing import List, Iterable, Sequence, Union, Callable, Dict @@ -53,17 +55,19 @@ def run(*funcs, **kwargs): :param parsers: Dictionary mapping types to parsers to use for parsing function arguments. :type parsers: Dict[type, Callable[[str], type]] - :param short: Dictionary mapping parameter names to letters to use as - alternative short flags. + :param short: Dictionary mapping parameter names (after conversion of + underscores to dashes) to letters, to use as alternative short flags. :type short: Dict[str, str] - :param List[str] argv: Command line arguments to parse (default: sys.argv[1:]) + :param List[str] argv: Command line arguments to parse (default: + sys.argv[1:]) :return: The value returned by the function that was run """ parsers = kwargs.pop('parsers', None) short = kwargs.pop('short', {}) argv = kwargs.pop('argv', None) if kwargs: - raise TypeError('unexpected keyword argument: {}'.format(list(kwargs)[0])) + raise TypeError( + 'unexpected keyword argument: {}'.format(list(kwargs)[0])) if not funcs: raise ValueError('need at least one function to run') if argv is None: @@ -77,7 +81,8 @@ def run(*funcs, **kwargs): else: subparsers = parser.add_subparsers() for func in funcs: - subparser = subparsers.add_parser(func.__name__, formatter_class=_Formatter) + subparser = subparsers.add_parser( + func.__name__, formatter_class=_Formatter) _populate_parser(func, subparser, parsers, short) subparser.set_defaults(_func=func) args = parser.parse_args(argv) @@ -111,7 +116,8 @@ def _populate_parser(func, parser, parsers, short): hasdefault = param.default != param.empty default = param.default if hasdefault else SUPPRESS required = not hasdefault and param.kind != param.VAR_POSITIONAL - positional = not hasdefault and not type_.container and param.kind != param.KEYWORD_ONLY + positional = (not hasdefault and not type_.container + and param.kind != param.KEYWORD_ONLY) if type_.type == bool and not positional and not type_.container: # Special case: just add parameterless --name and --no-name flags. group = parser.add_mutually_exclusive_group(required=required) @@ -143,7 +149,8 @@ def _populate_parser(func, parser, parsers, short): kwargs['default'] = [] if inspect.isclass(type_.type) and issubclass(type_.type, Enum): # Want these to behave like argparse choices. - kwargs['choices'] = _ValueOrderedDict((x.name, x) for x in type_.type) + kwargs['choices'] = _ValueOrderedDict( + (x.name, x) for x in type_.type) kwargs['type'] = _enum_getter(type_.type) else: kwargs['type'] = _get_parser(type_.type, parsers) @@ -193,7 +200,8 @@ def _get_type_from_doc(name, globalns): if match: container, type_ = match.groups() if container != 'list': - raise ValueError('unsupported container type: {}'.format(container)) + raise ValueError( + 'unsupported container type: {}'.format(container)) return _Type(eval(type_, globalns), list) return _get_type_from_hint(eval(name, globalns)) diff --git a/setup.cfg b/setup.cfg index 0dc630a..94b9c9a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,8 @@ [bdist_wheel] universal=1 -[nosetests] -with-coverage=1 -cover-erase=1 -cover-package=defopt -cover-min-percentage=98 +[coverage:run] +source=defopt + +[coverage:report] +fail_under=98 diff --git a/setup.py b/setup.py index 5fad85b..7cb1199 100644 --- a/setup.py +++ b/setup.py @@ -16,14 +16,16 @@ license='GNU General Public License v3', py_modules=['defopt'], test_suite='test_defopt', - install_requires=['docutils', 'sphinxcontrib-napoleon>=0.5.1'], - extras_require={ - ':python_version=="2.7"': ['enum34', 'funcsigs', 'typing'], - ':python_version=="3.3"': ['enum34', 'typing'], - ':python_version=="3.4"': ['typing'], - }, - tests_require=['coverage', 'mock'], - setup_requires=['nose'], + install_requires=[ + 'docutils', + 'sphinxcontrib-napoleon>=0.5.1', + 'funcsigs;python_version<"3"', + 'enum34;python_version<"3.4"', + 'typing;python_version<"3.5"', + ], + tests_require=[ + 'mock;python_version<"3"', + ], classifiers=[ 'Programming Language :: Python', 'Programming Language :: Python :: 2', @@ -32,6 +34,7 @@ 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Operating System :: OS Independent', 'Development Status :: 5 - Production/Stable', diff --git a/test_defopt.py b/test_defopt.py index 5fedbc8..f25214f 100644 --- a/test_defopt.py +++ b/test_defopt.py @@ -6,7 +6,10 @@ import typing import unittest -import mock +try: + from unittest import mock +except ImportError: + import mock import defopt from examples import booleans, choices, lists, parsers, short, starargs, styles @@ -30,8 +33,10 @@ def sub1(*bar): def sub2(baz=None): """:type baz: int""" return baz - self.assertEqual(defopt.run(sub1, sub2, argv=['sub1', '1.1']), (1.1,)) - self.assertEqual(defopt.run(sub1, sub2, argv=['sub2', '--baz', '1']), 1) + self.assertEqual( + defopt.run(sub1, sub2, argv=['sub1', '1.1']), (1.1,)) + self.assertEqual( + defopt.run(sub1, sub2, argv=['sub2', '--baz', '1']), 1) def test_var_positional(self): def main(*foo): @@ -118,7 +123,8 @@ def main(a_b_c, d_e_f=None): :type d_e_f: int """ return a_b_c, d_e_f - self.assertEqual(defopt.run(main, argv=['1', '--d-e-f', '2']), (1, 2)) + self.assertEqual( + defopt.run(main, argv=['1', '--d-e-f', '2']), (1, 2)) class TestParsers(unittest.TestCase): @@ -135,7 +141,8 @@ def parser(string): def main(value): """:type value: int""" return value - self.assertEqual(defopt.run(main, parsers={int: parser}, argv=['1']), 2) + self.assertEqual( + defopt.run(main, parsers={int: parser}, argv=['1']), 2) def test_parse_bool(self): parser = defopt._get_parser(bool) @@ -153,7 +160,8 @@ def test_list(self): def main(foo): """:type foo: list[float]""" return foo - self.assertEqual(defopt.run(main, argv=['--foo', '1.1', '2.2']), [1.1, 2.2]) + self.assertEqual( + defopt.run(main, argv=['--foo', '1.1', '2.2']), [1.1, 2.2]) def test_list_kwarg(self): def main(foo=None): @@ -162,7 +170,8 @@ def main(foo=None): :type foo: list[float] """ return foo - self.assertEqual(defopt.run(main, argv=['--foo', '1.1', '2.2']), [1.1, 2.2]) + self.assertEqual( + defopt.run(main, argv=['--foo', '1.1', '2.2']), [1.1, 2.2]) def test_list_bare(self): with self.assertRaises(ValueError): @@ -206,22 +215,27 @@ def test_bool_list(self): def main(foo): """:type foo: list[bool]""" return foo - self.assertEqual(defopt.run(main, argv=['--foo', '1', '0']), [True, False]) + self.assertEqual( + defopt.run(main, argv=['--foo', '1', '0']), [True, False]) def test_bool_var_positional(self): def main(*foo): """:type foo: bool""" return foo - self.assertEqual(defopt.run(main, argv=['1', '1', '0']), (True, True, False)) - self.assertEqual(defopt.run(main, argv=[]), ()) + self.assertEqual( + defopt.run(main, argv=['1', '1', '0']), (True, True, False)) + self.assertEqual( + defopt.run(main, argv=[]), ()) def test_bool_list_var_positional(self): def main(*foo): """:type foo: list[bool]""" return foo argv = ['--foo', '1', '--foo', '0', '0'] - self.assertEqual(defopt.run(main, argv=argv), ([True], [False, False])) - self.assertEqual(defopt.run(main, argv=[]), ()) + self.assertEqual( + defopt.run(main, argv=argv), ([True], [False, False])) + self.assertEqual( + defopt.run(main, argv=[]), ()) def test_bool_kwarg(self): default = object() @@ -233,7 +247,8 @@ def main(foo=default): self.assertIs(defopt.run(main, argv=['--no-foo']), False) self.assertIs(defopt.run(main, argv=[]), default) - @unittest.skipIf(sys.version_info < (3, 4), 'expectedFailure ignores SystemExit') + @unittest.skipIf(sys.version_info < (3, 4), + 'expectedFailure ignores SystemExit') @unittest.expectedFailure def test_bool_kwarg_override(self): def main(foo=True): @@ -300,8 +315,10 @@ def sub2(bar): """:type bar: Choice""" return bar - self.assertEqual(defopt.run(sub1, sub2, argv=['sub1', 'one']), Choice.one) - self.assertEqual(defopt.run(sub1, sub2, argv=['sub2', 'two']), Choice.two) + self.assertEqual( + defopt.run(sub1, sub2, argv=['sub1', 'one']), Choice.one) + self.assertEqual( + defopt.run(sub1, sub2, argv=['sub2', 'two']), Choice.two) def test_valuedict(self): valuedict = defopt._ValueOrderedDict({'a': 1}) @@ -476,7 +493,8 @@ def func(arg1, arg2, arg3=None): self._check_doc(doc) def _check_doc(self, doc): - self.assertEqual(doc.text, 'One line summary.\n\nExtended description.') + self.assertEqual( + doc.text, 'One line summary.\n\nExtended description.') self.assertEqual(len(doc.params), 3) self.assertEqual(doc.params['arg1'].text, 'Description of arg1') self.assertEqual(doc.params['arg1'].type, 'int') @@ -653,9 +671,11 @@ def test_booleans(self, print_): print_.assert_called_with('TEST') def test_booleans_cli(self): - output = self._run_example(booleans, ['test', '--no-upper', '--repeat']) + output = self._run_example( + booleans, ['test', '--no-upper', '--repeat']) self.assertEqual(output, b'test\ntest\n') - output = self._run_example(booleans, ['test']) + output = self._run_example( + booleans, ['test']) self.assertEqual(output, b'TEST\n') @unittest.skipIf(sys.version_info.major == 2, 'print is unpatchable') @@ -687,9 +707,11 @@ def test_lists(self, print_): print_.assert_called_with([2, 4, 6]) def test_lists_cli(self): - output = self._run_example(lists, ['2', '--numbers', '1.2', '3.4']) + output = self._run_example( + lists, ['2', '--numbers', '1.2', '3.4']) self.assertEqual(output, b'[2.4, 6.8]\n') - output = self._run_example(lists, ['--numbers', '1.2', '3.4', '--', '2']) + output = self._run_example( + lists, ['--numbers', '1.2', '3.4', '--', '2']) self.assertEqual(output, b'[2.4, 6.8]\n') @unittest.skipIf(sys.version_info.major == 2, 'print is unpatchable') @@ -757,3 +779,7 @@ def _run_example(self, example, argv): argv = [sys.executable, '-m', example.__name__] + argv output = subprocess.check_output(argv, stderr=subprocess.STDOUT) return output.replace(b'\r\n', b'\n') + + +if __name__ == "__main__": + unittest.main()