Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import & test fixes. #20

Merged
merged 4 commits into from
Mar 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@ target/

# PyCharm files
.idea/

# Editor files
.*.swp
5 changes: 3 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 18 additions & 10 deletions defopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))

Expand Down
10 changes: 5 additions & 5 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -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
19 changes: 11 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
66 changes: 46 additions & 20 deletions test_defopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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()
Expand All @@ -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):
Expand Down Expand Up @@ -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})
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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()