Skip to content

Commit

Permalink
*100%* test coverage in modular!
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucretiel committed Feb 6, 2016
1 parent c68c0da commit a857af9
Show file tree
Hide file tree
Showing 16 changed files with 499 additions and 13 deletions.
17 changes: 17 additions & 0 deletions src/autocommand/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# Copyright 2014-2016 Nathan West
#
# This file is part of autocommand.
#
# autocommand is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# autocommand is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.

from .automain import automain
from .autoparse import autoparse, smart_open
from .autocommand import autocommand
Expand Down
14 changes: 9 additions & 5 deletions src/autocommand/autoparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,15 @@ def _get_type_description(annotation):
elif isinstance(annotation, str):
return None, annotation
elif isinstance(annotation, tuple):
arg1, arg2 = annotation
if isinstance(arg1, type) and isinstance(arg2, str):
return arg1, arg2
elif isinstance(arg1, str) and isinstance(arg2, type):
return arg2, arg1
try:
arg1, arg2 = annotation
except ValueError as e:
raise AnnotationError(annotation) from e
else:
if isinstance(arg1, type) and isinstance(arg2, str):
return arg1, arg2
elif isinstance(arg1, str) and isinstance(arg2, type):
return arg2, arg1

raise AnnotationError(annotation)

Expand Down
18 changes: 18 additions & 0 deletions src/autocommand/errors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
# Copyright 2014-2016 Nathan West
#
# This file is part of autocommand.
#
# autocommand is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# autocommand is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.


class AutocommandError(Exception):
'''Base class for autocommand exceptions'''
pass
Expand Down
17 changes: 17 additions & 0 deletions test/test_autoasync.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# Copyright 2014-2016 Nathan West
#
# This file is part of autocommand.
#
# autocommand is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# autocommand is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.

import pytest
from contextlib import closing, contextmanager
asyncio = pytest.importorskip('asyncio')
Expand Down
31 changes: 29 additions & 2 deletions test/test_autocommand.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,40 @@
# Copyright 2014-2016 Nathan West
#
# This file is part of autocommand.
#
# autocommand is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# autocommand is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.

import pytest
import sys
from unittest.mock import patch, sentinel
from autocommand import autocommand


autocommand_module = sys.modules['autocommand.autocommand']


def _asyncio_unavailable():
try:
import asyncio
except ImportError:
return True
else:
return False

uses_async = pytest.mark.skipif(
sys.version_info < (3, 4),
reason="async tests require python 3.4+")
_asyncio_unavailable(),
reason="async tests require asyncio (python3.4+)")


@pytest.yield_fixture
Expand Down
17 changes: 17 additions & 0 deletions test/test_automain.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
# Copyright 2014-2016 Nathan West
#
# This file is part of autocommand.
#
# autocommand is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# autocommand is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with autocommand. If not, see <http://www.gnu.org/licenses/>.

import pytest
from autocommand.automain import automain, AutomainRequiresModuleError

Expand Down
6 changes: 6 additions & 0 deletions test/test_autoparse/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
`test_autoparse`
================

It turns out that autoparse is somewhat difficult to purely unit test, because a lot of the functionality is orthogonal (typing, type deduction, argument-vs-option, flag letter assignment), but the orthogonal parts are difficult to isolate implementation and API-wise. Therefore, rather than unit-test each component with mocking so on, even though we test each orthogonal unit of functionality more or less in separate files, we understand that something failing in, say, flag letter assignment could cause a failure in type deduction tests.

In the grand scheme of things, autoparse really isn't that complicated, so hopefully a cascading test failure can be easily tracked down no matter what.
34 changes: 34 additions & 0 deletions test/test_autoparse/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import pytest
from inspect import signature
from autocommand.autoparse import make_parser


@pytest.fixture(scope='session')
def check_parse():
def check_parse_impl(function, *args, add_nos=False, **kwargs):
parser = make_parser(
func_sig=signature(function),
description=None,
epilog=None,
add_nos=add_nos)

parsed_args = parser.parse_args(args)
for key, value in kwargs.items():
assert getattr(parsed_args, key) == value

return check_parse_impl


# This is ostensibly session scope, but I don't know how capsys works
@pytest.fixture
def check_help_text(capsys):
def check_help_text_impl(func, *texts):
with pytest.raises(SystemExit):
func()

out, err = capsys.readouterr()

# TODO: be wary of argparse's text formatting
for text in texts:
assert text in out or text in err
return check_help_text_impl
38 changes: 38 additions & 0 deletions test/test_autoparse/test_annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest
from autocommand.autoparse import AnnotationError


def test_all_annotation_types(check_parse, check_help_text):
def func(
int_arg: int,
note_arg: "note_arg description",
note_int: ("note_int description", int),
int_note: (int, "int_note description")): pass

check_help_text(
lambda: check_parse(func, '-h'),
"note_arg description",
"note_int description",
"int_note description")

check_parse(
func,
"1", "2", "3", "4",
int_arg=1, note_arg="2", note_int=3, int_note=4)


@pytest.mark.parametrize('bad_annotation', [
1000, # An int? What?
{'foo': 'bar'}, # A dict isn't any better
[int, 'fooo'], # For implementation ease we do ask for a tuple
(), # Though the tuple should have things in it
(int,), # More things
(int, 'hello', 'world'), # TOO MANY THINGS
(int, int), # The wrong kinds of things
("hello", "world"), # Nope this is bad too
])
def test_bad_annotation(bad_annotation, check_parse):
def func(arg: bad_annotation): pass

with pytest.raises(AnnotationError):
check_parse(func)
20 changes: 20 additions & 0 deletions test/test_autoparse/test_bad_signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from inspect import signature, Signature, Parameter
import pytest
from autocommand.autoparse import KWArgError, PositionalArgError, make_parser


def test_kwargs(check_parse):
def func(**kwargs): pass

with pytest.raises(KWArgError):
make_parser(signature(func), "", "", False)


def test_positional(check_parse):
# We have to fake this one, because it isn't possible to create a
# positional-only parameter in pure python

with pytest.raises(PositionalArgError):
make_parser(
Signature([Parameter('arg', Parameter.POSITIONAL_ONLY)]),
"", "", False)
45 changes: 45 additions & 0 deletions test/test_autoparse/test_basic_autoparse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest


def test_basic_positional(check_parse):
def func(arg): pass

check_parse(
func,
"foo",
arg="foo")


@pytest.mark.parametrize('cli_args', [
[],
['value'],
['value1', 'value2']
])
def test_variadic_positional(check_parse, cli_args):
check_parse(
lambda arg1, *args: None,
'arg1_value', *cli_args,
arg1='arg1_value', args=cli_args)


def test_optional_default(check_parse):
check_parse(
lambda arg="default_value": None,
arg="default_value")


@pytest.mark.parametrize('flags, result', [
([], False),
(['-f'], True),
(['--no-flag'], False),
(['--no-flag', '-f'], True),
(['--flag', '--no-flag'], False)
])
def test_add_nos(check_parse, flags, result):
def func(flag: bool): pass

check_parse(
func,
*flags,
add_nos=True,
flag=result)
78 changes: 78 additions & 0 deletions test/test_autoparse/test_invocation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from unittest.mock import patch
from argparse import ArgumentParser
import pytest
from autocommand.autoparse import autoparse


def test_basic_invocation():
@autoparse
def func(arg1, arg2: int, opt1=None, opt2=2, opt3='default', flag=False):
return arg1, arg2, opt1, opt2, opt3, flag

arg1, arg2, opt1, opt2, opt3, flag = func(
['value1', '1', '-o', 'hello', '--opt2', '10', '-f'])

assert arg1 == 'value1'
assert arg2 == 1
assert opt1 == 'hello'
assert opt2 == 10
assert opt3 == 'default'
assert flag is True


def test_invocation_from_argv():
@autoparse
def func(arg1, arg2: int):
return arg1, arg2

with patch('sys.argv', ['command', '1', '2']):
arg1, arg2 = func()
assert arg1 == "1"
assert arg2 == 2


def test_description_epilog_help(check_help_text):
@autoparse(
description='this is a description',
epilog='this is an epilog')
def func(arg: 'this is help text'):
pass

check_help_text(
lambda: func(['-h']),
'this is a description',
'this is an epilog',
'this is help text')


def test_docstring_description(check_help_text):
@autoparse
def func(arg):
'''This is a docstring description'''
pass

check_help_text(
lambda: func(['-h']),
'This is a docstring description')


def test_custom_parser():
parser = ArgumentParser()

parser.add_argument('arg', nargs='?')
group = parser.add_mutually_exclusive_group()
group.add_argument('-v', '--verbose', action='store_true')
group.add_argument('-q', '--quiet', action='store_true')

@autoparse(parser=parser)
def main(arg, verbose, quiet):
return arg, verbose, quiet

assert main([]) == (None, False, False)
assert main(['thing']) == ('thing', False, False)
assert main(['-v']) == (None, True, False)
assert main(['-q']) == (None, False, True)
assert main(['-v', 'thing']) == ('thing', True, False)

with pytest.raises(SystemExit):
main(['-v', '-q'])
Loading

0 comments on commit a857af9

Please sign in to comment.