From 684fb158f31c9bb0fc0b8048624f7ad1f60ad06a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 4 Jun 2016 16:04:01 -0400 Subject: [PATCH 1/2] TST: switch to using pytest - also start using codecov --- .coveragerc | 6 +++ .travis.yml | 7 +-- appveyor.yml | 2 +- run_tests.py | 32 ++++---------- test_cycler.py | 118 ++++++++++++++++++++++++------------------------- 5 files changed, 77 insertions(+), 88 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..dbcb44a --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[run] +source=cycler +branch=True +[report] +omit = + *test* diff --git a/.travis.yml b/.travis.yml index c354030..b0669fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,10 +12,11 @@ matrix: install: - python setup.py install - - pip install coveralls six + - pip install pytest pytest-cov coverage six script: - - python run_tests.py + - coverage run run_tests.py + - coverage report -m after_success: - coveralls + - bash <(curl -s https://codecov.io/bash) diff --git a/appveyor.yml b/appveyor.yml index 2cddd81..220fa3a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -44,7 +44,7 @@ install: - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # Install the build and runtime dependencies of the project. - - "%CMD_IN_ENV% pip install -v six nose coveralls" + - "%CMD_IN_ENV% pip install -v six nose pytest pytest-cov coverage" # Install the generated wheel package to test it - "python setup.py install" diff --git a/run_tests.py b/run_tests.py index c47d8c7..7efe075 100644 --- a/run_tests.py +++ b/run_tests.py @@ -1,27 +1,11 @@ #!/usr/bin/env python -# This file is closely based on tests.py from matplotlib -# -# This allows running the matplotlib tests from the command line: e.g. -# -# $ python run_tests.py -v -d -# -# The arguments are identical to the arguments accepted by nosetests. -# -# See https://nose.readthedocs.org/ for a detailed description of -# these options. -import nose - - -env = {"NOSE_WITH_COVERAGE": 1, - 'NOSE_COVER_PACKAGE': ['cycler'], - 'NOSE_COVER_HTML': 1} -plugins = [] - - -def run(): - - nose.main(addplugins=[x() for x in plugins], env=env) - +import sys +import pytest if __name__ == '__main__': - run() + # show output results from every test function + args = [] + args.extend(sys.argv[1:]) + # call pytest and exit with the return code from pytest so that + # travis will fail correctly if tests fail + sys.exit(pytest.main(args)) diff --git a/test_cycler.py b/test_cycler.py index 9c155b4..52f65ec 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -3,25 +3,23 @@ import six from six.moves import zip, range from cycler import cycler, Cycler, concat -from nose.tools import (assert_equal, assert_not_equal, - assert_raises, assert_true) +import pytest from itertools import product, cycle, chain from operator import add, iadd, mul, imul from collections import defaultdict def _cycler_helper(c, length, keys, values): - assert_equal(len(c), length) - assert_equal(len(c), len(list(c))) - assert_equal(c.keys, set(keys)) - + assert len(c) == length + assert len(c) == len(list(c)) + assert c.keys == set(keys) for k, vals in zip(keys, values): for v, v_target in zip(c, vals): - assert_equal(v[k], v_target) + assert v[k] == v_target def _cycles_equal(c1, c2): - assert_equal(list(c1), list(c2)) + assert list(c1) == list(c2) def test_creation(): @@ -42,8 +40,10 @@ def test_compose(): yield _cycler_helper, c2+c1, 3, ['c', 'lw'], [list('rgb'), range(3)] yield _cycles_equal, c2+c1, c1+c2 # miss-matched add lengths - assert_raises(ValueError, add, c1, c3) - assert_raises(ValueError, add, c3, c1) + with pytest.raises(ValueError): + c1 + c3 + with pytest.raises(ValueError): + c3 + c1 # multiplication target = zip(*product(list('rgb'), range(3))) @@ -92,16 +92,16 @@ def test_constructor(): def test_failures(): c1 = cycler(c='rgb') c2 = cycler(c=c1) - assert_raises(ValueError, add, c1, c2) - assert_raises(ValueError, iadd, c1, c2) - assert_raises(ValueError, mul, c1, c2) - assert_raises(ValueError, imul, c1, c2) - assert_raises(TypeError, iadd, c2, 'aardvark') - assert_raises(TypeError, imul, c2, 'aardvark') + pytest.raises(ValueError, add, c1, c2) + pytest.raises(ValueError, iadd, c1, c2) + pytest.raises(ValueError, mul, c1, c2) + pytest.raises(ValueError, imul, c1, c2) + pytest.raises(TypeError, iadd, c2, 'aardvark') + pytest.raises(TypeError, imul, c2, 'aardvark') c3 = cycler(ec=c1) - assert_raises(ValueError, cycler, c=c2+c3) + pytest.raises(ValueError, cycler, c=c2+c3) def test_simplify(): @@ -123,9 +123,9 @@ def test_multiply(): def test_mul_fails(): c1 = cycler(c='rgb') - assert_raises(TypeError, mul, c1, 2.0) - assert_raises(TypeError, mul, c1, 'a') - assert_raises(TypeError, mul, c1, []) + pytest.raises(TypeError, mul, c1, 2.0) + pytest.raises(TypeError, mul, c1, 'a') + pytest.raises(TypeError, mul, c1, []) def test_getitem(): @@ -140,15 +140,14 @@ def test_getitem(): def test_fail_getime(): c1 = cycler(lw=range(15)) - assert_raises(ValueError, Cycler.__getitem__, c1, 0) - assert_raises(ValueError, Cycler.__getitem__, c1, [0, 1]) + pytest.raises(ValueError, Cycler.__getitem__, c1, 0) + pytest.raises(ValueError, Cycler.__getitem__, c1, [0, 1]) def _repr_tester_helper(rpr_func, cyc, target_repr): test_repr = getattr(cyc, rpr_func)() - assert_equal(six.text_type(test_repr), - six.text_type(target_repr)) + assert six.text_type(test_repr) == six.text_type(target_repr) def test_repr(): @@ -172,13 +171,13 @@ def test_repr(): def test_call(): c = cycler(c='rgb') c_cycle = c() - assert_true(isinstance(c_cycle, cycle)) + assert isinstance(c_cycle, cycle) j = 0 for a, b in zip(2*c, c_cycle): j += 1 - assert_equal(a, b) + assert a == b - assert_equal(j, len(c) * 2) + assert j == len(c) * 2 def test_copying(): @@ -202,21 +201,21 @@ def test_copying(): c_after = (c1 + c2) * c3 - assert_equal(c1, cycler('c', [1, 2, 3])) - assert_equal(c2, cycler('lw', ['r', 'g', 'b'])) - assert_equal(c3, cycler('foo', [['y', 'g', 'blue'], ['b', 'k']])) - assert_equal(c_before, (cycler(c=[1, 2, 3], lw=['r', 'g', 'b']) * - cycler('foo', [['y', 'g', 'blue'], ['b', 'k']]))) - assert_equal(c_after, (cycler(c=[1, 2, 3], lw=['r', 'g', 'b']) * - cycler('foo', [['y', 'g', 'blue'], ['b', 'k']]))) + assert c1 == cycler('c', [1, 2, 3]) + assert c2 == cycler('lw', ['r', 'g', 'b']) + assert c3 == cycler('foo', [['y', 'g', 'blue'], ['b', 'k']]) + assert c_before == (cycler(c=[1, 2, 3], lw=['r', 'g', 'b']) * + cycler('foo', [['y', 'g', 'blue'], ['b', 'k']])) + assert c_after == (cycler(c=[1, 2, 3], lw=['r', 'g', 'b']) * + cycler('foo', [['y', 'g', 'blue'], ['b', 'k']])) # Make sure that changing the key for a specific cycler # doesn't break things for a composed cycler c = (c1 + c2) * c3 c4 = cycler('bar', c3) - assert_equal(c, (cycler(c=[1, 2, 3], lw=['r', 'g', 'b']) * - cycler('foo', [['y', 'g', 'blue'], ['b', 'k']]))) - assert_equal(c3, cycler('foo', [['y', 'g', 'blue'], ['b', 'k']])) + assert c == (cycler(c=[1, 2, 3], lw=['r', 'g', 'b']) * + cycler('foo', [['y', 'g', 'blue'], ['b', 'k']])) + assert c3 == cycler('foo', [['y', 'g', 'blue'], ['b', 'k']]) def test_keychange(): @@ -225,35 +224,35 @@ def test_keychange(): c3 = cycler('ec', 'yk') c3.change_key('ec', 'edgecolor') - assert_equal(c3, cycler('edgecolor', c3)) + assert c3 == cycler('edgecolor', c3) c = c1 + c2 c.change_key('lw', 'linewidth') # Changing a key in one cycler should have no # impact in the original cycler. - assert_equal(c2, cycler('lw', [1, 2, 3])) - assert_equal(c, c1 + cycler('linewidth', c2)) + assert c2 == cycler('lw', [1, 2, 3]) + assert c == c1 + cycler('linewidth', c2) c = (c1 + c2) * c3 c.change_key('c', 'color') - assert_equal(c1, cycler('c', 'rgb')) - assert_equal(c, (cycler('color', c1) + c2) * c3) + assert c1 == cycler('c', 'rgb') + assert c == (cycler('color', c1) + c2) * c3 # Perfectly fine, it is a no-op c.change_key('color', 'color') - assert_equal(c, (cycler('color', c1) + c2) * c3) + assert c == (cycler('color', c1) + c2) * c3 # Can't change a key to one that is already in there - assert_raises(ValueError, Cycler.change_key, c, 'color', 'lw') + pytest.raises(ValueError, Cycler.change_key, c, 'color', 'lw') # Can't change a key you don't have - assert_raises(KeyError, Cycler.change_key, c, 'c', 'foobar') + pytest.raises(KeyError, Cycler.change_key, c, 'c', 'foobar') def _eq_test_helper(a, b, res): if res: - assert_equal(a, b) + assert a == b else: - assert_not_equal(a, b) + assert a != b def test_eq(): @@ -273,34 +272,34 @@ def test_eq(): def test_cycler_exceptions(): - assert_raises(TypeError, cycler) - assert_raises(TypeError, cycler, 'c', 'rgb', lw=range(3)) - assert_raises(TypeError, cycler, 'c') - assert_raises(TypeError, cycler, 'c', 'rgb', 'lw', range(3)) + pytest.raises(TypeError, cycler) + pytest.raises(TypeError, cycler, 'c', 'rgb', lw=range(3)) + pytest.raises(TypeError, cycler, 'c') + pytest.raises(TypeError, cycler, 'c', 'rgb', 'lw', range(3)) def test_starange_init(): c = cycler('r', 'rgb') c2 = cycler('lw', range(3)) cy = Cycler(list(c), list(c2), zip) - assert_equal(cy, c + c2) + assert cy == c + c2 def test_concat(): a = cycler('a', range(3)) b = cycler('a', 'abc') for con, chn in zip(a.concat(b), chain(a, b)): - assert_equal(con, chn) + assert con == chn for con, chn in zip(concat(a, b), chain(a, b)): - assert_equal(con, chn) + assert con == chn def test_concat_fail(): a = cycler('a', range(3)) b = cycler('b', range(3)) - assert_raises(ValueError, concat, a, b) - assert_raises(ValueError, a.concat, b) + pytest.raises(ValueError, concat, a, b) + pytest.raises(ValueError, a.concat, b) def _by_key_helper(cy): @@ -310,14 +309,14 @@ def _by_key_helper(cy): for k, v in sty.items(): target[k].append(v) - assert_equal(res, target) + assert res == target def test_by_key_add(): input_dict = dict(c=list('rgb'), lw=[1, 2, 3]) cy = cycler(c=input_dict['c']) + cycler(lw=input_dict['lw']) res = cy.by_key() - assert_equal(res, input_dict) + assert res == input_dict yield _by_key_helper, cy @@ -325,8 +324,7 @@ def test_by_key_mul(): input_dict = dict(c=list('rg'), lw=[1, 2, 3]) cy = cycler(c=input_dict['c']) * cycler(lw=input_dict['lw']) res = cy.by_key() - assert_equal(input_dict['lw'] * len(input_dict['c']), - res['lw']) + assert input_dict['lw'] * len(input_dict['c']) == res['lw'] yield _by_key_helper, cy From 698cd0c97ee4ee6fdb5ac2b14cd363329ca07728 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 4 Jun 2016 22:08:23 -0400 Subject: [PATCH 2/2] ENH: cycler with a tail Add a generator function to add an infinite 'tail' onto a cycler. Closes #30 --- cycler.py | 29 +++++++++++++++++++++++++++++ doc/source/index.rst | 9 +++++---- test_cycler.py | 17 ++++++++++++++++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/cycler.py b/cycler.py index 8400444..f9191eb 100644 --- a/cycler.py +++ b/cycler.py @@ -558,3 +558,32 @@ def _cycler(label, itr): itr = (v[lab] for v in itr) return Cycler._from_iter(label, itr) + + +def cycler_with_tail(cyl, tail): + '''After the cycle is exhausted continue to yield tail + + + Parameters + ---------- + cyl : Cycler + The cycler to iterate through + + tail : dict + The dictionary to yield. Must have the same keys as ``cyl``. + + Yields + ------ + sty : dict + ''' + tk = set(tail.keys()) + if cyl.keys != tk: + raise RuntimeError('The cycler has keys {} and the tail {}. The ' + 'different keys are {}'.format(cyl.keys, + tk, tk ^ cyl.keys)) + + for sty in cyl: + yield sty + + while True: + yield tail diff --git a/doc/source/index.rst b/doc/source/index.rst index d584ebf..629f648 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -26,12 +26,13 @@ github https://github.com/matplotlib/cycler cycler Cycler concat + cycler_with_tail The public API of :py:mod:`cycler` consists of a class `Cycler`, a -factory function :func:`cycler`, and a concatenation function -:func:`concat`. The factory function provides a simple interface for -creating 'base' `Cycler` objects while the class takes care of the -composition and iteration logic. +factory function :func:`cycler`, a concatenation function +:func:`concat`, and a few helper functions. The factory function +provides a simple interface for creating 'base' `Cycler` objects while +the class takes care of the composition and iteration logic. `Cycler` Usage diff --git a/test_cycler.py b/test_cycler.py index 52f65ec..b86f7d6 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -2,7 +2,7 @@ import six from six.moves import zip, range -from cycler import cycler, Cycler, concat +from cycler import cycler, Cycler, concat, cycler_with_tail import pytest from itertools import product, cycle, chain from operator import add, iadd, mul, imul @@ -341,3 +341,18 @@ def test_contains(): assert 'a' in ab assert 'b' in ab + + +def test_tail(): + a = cycler('a', range(3)) + tail = {'a': 4} + + cy_with_tail = cycler_with_tail(a, tail) + for j in range(3): + next(cy_with_tail) + + for j in range(5): + assert {'a': 4} == next(cy_with_tail) + + with pytest.raises(RuntimeError): + next(cycler_with_tail(a, {'b': 1}))