Skip to content

Commit

Permalink
release prep, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
vreuter committed May 2, 2019
1 parent 6d50c00 commit b0cc865
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 7 deletions.
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [0.0.3] - 2019-05-02
### Added
- CLI optarg string builder (`build_cli_extra`)
- `powerset` (all subsets of a collection)

## [0.0.2] - 2019-05-01
## Changed
Expand Down
20 changes: 17 additions & 3 deletions tests/test_build_cli_extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,34 @@

from collections import OrderedDict
import pytest
from ubiquerg import build_cli_extra
from ubiquerg import build_cli_extra, powerset

__author__ = "Vince Reuter"
__email__ = "vreuter@virginia.edu"


def pytest_generate_tests(metafunc):
""" Test case generation and parameterization for this module """
if "ordwrap" in metafunc.fixturenames:
metafunc.parametrize("ordwrap", [tuple, OrderedDict])


@pytest.mark.parametrize(["optargs", "expected"], [
([("-X", None), ("--revert", 1), ("-O", "outfile"),
("--execute-locally", None), ("-I", ["file1", "file2"])],
"-X --revert 1 -O outfile --execute-locally -I file1 file2")
])
def test_build_cli_extra(optargs, expected):
def test_build_cli_extra(optargs, expected, ordwrap):
""" Check that CLI optargs are rendered as expected. """
observed = build_cli_extra(**OrderedDict(optargs))
observed = build_cli_extra(ordwrap(optargs))
print("expected: {}".format(expected))
print("observed: {}".format(observed))
assert expected == observed


@pytest.mark.parametrize(
"optargs", powerset([(None, "a"), (1, "one")], nonempty=True))
def test_illegal_cli_extra_input_is_exceptional(optargs, ordwrap):
""" Non-string keys are illegal and cause a TypeError. """
with pytest.raises(TypeError):
build_cli_extra(ordwrap(optargs))
58 changes: 57 additions & 1 deletion tests/test_collection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
""" Tests for collection utilities """

from collections import OrderedDict
import inspect
import random
import string
import sys
Expand All @@ -10,6 +12,36 @@
__email__ = "vreuter@virginia.edu"


def get_default_parameters(func, pred=None):
"""
For given function, get mapping from parameter name to default value.
:param callable func: the function to inspect
:param func(object, object) -> bool pred: how to determine whether the
parameter should be included, based on name and default value
:return OrderedDict[str, object]]: mapping from parameter name to default value
"""
if not callable(func):
raise TypeError("Not a callable: {} ({})".format(func.__name__, type(func)))
spec = inspect.getfullargspec(func)
par_arg_pairs = zip(spec.args[(len(spec.args) - len(spec.defaults)):],
spec.defaults)
return OrderedDict(
par_arg_pairs if pred is None else
[(p, a) for p, a in par_arg_pairs if pred(p, a)])


POWERSET_BOOL_KWARGSPACE = {
p: [False, True] for p in
get_default_parameters(powerset, lambda _, v: isinstance(v, bool))}


def pytest_generate_tests(metafunc):
""" Test case generation and parameterization for this module """
if "arbwrap" in metafunc.fixturenames:
metafunc.parametrize("arbwrap", [list, tuple, set, iter])


def randcoll(pool, dt):
"""
Generate random collection of 1-10 elements.
Expand Down Expand Up @@ -38,6 +70,30 @@ def randcoll(pool, dt):
(randcoll([int(d) for d in string.digits], set), True),
("", False), ("abc", False)]
)
def test_coll_like(arg, exp):
def test_is_collection_like(arg, exp):
""" Test arbiter of whether an object is collection-like. """
assert exp == is_collection_like(arg)


@pytest.mark.skip("not implemented")
@pytest.mark.parametrize("kwargs", [])
def test_powerset_of_empty_pool(arbwrap, kwargs):
assert [] == powerset(arbwrap([]), **kwargs)


@pytest.mark.skip("not implemented")
def test_powerset_fewer_items_than_min(pool, arbwrap, kwargs):
assert [] == powerset(arbwrap(pool), **kwargs)


@pytest.mark.skip("not implemented")
@pytest.mark.parametrize(["pool", "kwargs", "expected"], [])
def test_powerset_legitimate_input(arbwrap, pool, kwargs, expected):
observed = powerset(arbwrap(pool), **kwargs)
assert len(expected) == len(observed)
assert expected == set(observed)


@pytest.mark.skip("not implemented")
def test_powerset_illegal_input(arbwrap):
pass
21 changes: 21 additions & 0 deletions tests/test_this_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
""" Validate what's available directly on the top-level import. """

import pytest

__author__ = "Vince Reuter"
__email__ = "vreuter@virginia.edu"


@pytest.mark.parametrize(
["obj_name", "typecheck"],
[("build_cli_extra", callable), ("is_collection_like", callable),
("powerset", callable)])
def test_top_level_exports(obj_name, typecheck):
""" At package level, validate object availability and type. """
import ubiquerg
try:
obj = getattr(ubiquerg, obj_name)
except AttributeError:
pytest.fail("Unavailable on {}: {}".format(ubiquerg.__name__, obj_name))
else:
assert typecheck(obj)
15 changes: 13 additions & 2 deletions ubiquerg/cli_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
__all__ = ["build_cli_extra"]


def build_cli_extra(**kwargs):
def build_cli_extra(optargs):
"""
Render CLI options/args as text to add to base command.
Expand All @@ -17,14 +17,25 @@ def build_cli_extra(**kwargs):
with single space between each. All non-string values are converted to
string.
:param Mapping | Iterable[(str, object)] optargs: values used as
options/arguments
:return str: text to add to base command, based on given opts/args
:raise TypeError: if an option name isn't a string
"""

def render(k, v):
if not isinstance(k, str):
raise TypeError(
"Option name isn't a string: {} ({})".format(k, type(k)))
if v is None:
return k
if is_collection_like(v):
v = " ".join(map(str, v))
return "{} {}".format(k, v)

return " ".join(render(*kv) for kv in kwargs.items())
try:
data_iter = optargs.items()
except AttributeError:
data_iter = optargs

return " ".join(render(*kv) for kv in data_iter)
36 changes: 35 additions & 1 deletion ubiquerg/collection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
""" Tools for working with collections """

import itertools
import sys
if sys.version_info < (3, 3):
from collections import Iterable
Expand All @@ -10,7 +11,7 @@
__email__ = "vreuter@virginia.edu"


__all__ = ["is_collection_like"]
__all__ = ["is_collection_like", "powerset"]


def is_collection_like(c):
Expand All @@ -21,3 +22,36 @@ def is_collection_like(c):
:return bool: Whether the argument is a (non-string) collection
"""
return isinstance(c, Iterable) and not isinstance(c, str)


def powerset(items, min_items=None, include_full_pop=True, nonempty=False):
"""
Build the powerset of a collection of items.
:param Iterable[object] items: "Pool" of all items, the population for
which to build the power set.
:param int min_items: Minimum number of individuals from the population
to allow in any given subset.
:param bool include_full_pop: Whether to include the full population in
the powerset (default True to accord with genuine definition)
:param bool nonempty: force each subset returned to be nonempty
:return list[object]: Sequence of subsets of the population, in
nondecreasing size order
:raise TypeError: if minimum item count is specified but is not an integer
:raise ValueError: if minimum item count is insufficient to guarantee
nonempty subsets
"""
if min_items is None:
min_items = 1 if nonempty else 0
else:
if not isinstance(min_items, int):
raise TypeError("Min items count for each subset isn't an integer: "
"{} ({})".format(min_items, type(min_items)))
if nonempty and min_items < 1:
raise ValueError("When minimum item count is {}, nonempty subsets "
"cannot be guaranteed.".format(min_items))
items = list(items) # Account for iterable burn possibility.
max_items = len(items) + 1 if include_full_pop else len(items)
return list(itertools.chain.from_iterable(
itertools.combinations(items, k)
for k in range(min_items, max_items)))

0 comments on commit b0cc865

Please sign in to comment.