From ecaf0e9b6717b35c018cc8022ab950a206813b64 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 16 Feb 2016 00:03:19 -0500 Subject: [PATCH 01/98] DOC: add concat to top-level API table --- doc/source/index.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 911c403..8b10f0d 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -25,12 +25,13 @@ github https://github.com/matplotlib/cycler cycler Cycler + concat - -The public API of :py:mod:`cycler` consists of a class `Cycler` and a -factory function :func:`cycler`. The function provides a simple interface for -creating 'base' `Cycler` objects while the class takes care of the composition -and iteration logic. +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. `Cycler` Usage From 08493a0dd1b7d1d95cceb66aff4fa19e62663d30 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 17 Feb 2016 22:49:16 -0500 Subject: [PATCH 02/98] BLD: include test file in sdist closes #31 --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 815cfe5..24bc16d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include run_tests.py +include test_cycler.py include LICENSE recursive-include conda-recipe * recursive-include doc Makefile make.bat *.rst *.py \ No newline at end of file From e9d95e37f6793eac9c7e84cb223a99eb4d46f030 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 5 May 2016 22:42:46 -0400 Subject: [PATCH 03/98] API: drop py2.6 support --- cycler.py | 3 +-- setup.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/cycler.py b/cycler.py index 3c3eb2d..8050ca9 100644 --- a/cycler.py +++ b/cycler.py @@ -367,8 +367,7 @@ def by_key(self): # and if we care. keys = self.keys - # change this to dict comprehension when drop 2.6 - out = dict((k, list()) for k in keys) + out = {k: list() for k in keys} for d in self: for k in keys: diff --git a/setup.py b/setup.py index d70d29d..0cbff1c 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,6 @@ license="BSD", classifiers=['Development Status :: 4 - Beta', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', From 80a831b99d0170080bc27159ebcb8f4c88d64beb Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 22 May 2016 19:43:54 -0400 Subject: [PATCH 04/98] ENH: add `__contains__` to Cycler object support 'in' --- cycler.py | 3 +++ test_cycler.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/cycler.py b/cycler.py index 8050ca9..8ec34fc 100644 --- a/cycler.py +++ b/cycler.py @@ -135,6 +135,9 @@ def __init__(self, left, right=None, op=None): self._keys = _process_keys(self._left, self._right) self._op = op + def __contains__(self, k): + return k in self._keys + @property def keys(self): """ diff --git a/test_cycler.py b/test_cycler.py index f65a4cd..9c155b4 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -328,3 +328,18 @@ def test_by_key_mul(): assert_equal(input_dict['lw'] * len(input_dict['c']), res['lw']) yield _by_key_helper, cy + + +def test_contains(): + a = cycler('a', range(3)) + b = cycler('b', range(3)) + + assert 'a' in a + assert 'b' in b + assert 'a' not in b + assert 'b' not in a + + ab = a+b + + assert 'a' in ab + assert 'b' in ab From 1f63921996f0c6ff287e19b90118ed36b3653d7f Mon Sep 17 00:00:00 2001 From: "Adrien F. VINCENT" Date: Fri, 3 Jun 2016 16:48:51 +0200 Subject: [PATCH 05/98] DOC: Fix typos about itertools.product --- cycler.py | 2 +- doc/source/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cycler.py b/cycler.py index 8ec34fc..8400444 100644 --- a/cycler.py +++ b/cycler.py @@ -87,7 +87,7 @@ class Cycler(object): in-place ``+`` ``*`` - for outer products (itertools.product) and integer multiplication + for outer products (`itertools.product`) and integer multiplication ``*=`` in-place ``*`` diff --git a/doc/source/index.rst b/doc/source/index.rst index 8b10f0d..d584ebf 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -156,7 +156,7 @@ Any pair of `Cycler` can be multiplied m_c = m_cycle * color_cycle which gives the 'outer product' of the two cycles (same as -:func:`itertools.prod` ) +:func:`itertools.product` ) .. ipython:: python From 684fb158f31c9bb0fc0b8048624f7ad1f60ad06a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 4 Jun 2016 16:04:01 -0400 Subject: [PATCH 06/98] 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 8e663c4a1c4b764a040d8bd440c9e56f7e6b36de Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 15 Dec 2017 11:44:19 +0200 Subject: [PATCH 07/98] http -> https --- README.rst | 2 +- appveyor.yml | 4 ++-- ci/appveyor/install.ps1 | 4 ++-- ci/appveyor/run_with_env.cmd | 2 +- conda-recipe/bld.bat | 2 +- conda-recipe/build.sh | 2 +- conda-recipe/meta.yaml | 4 ++-- doc/source/conf.py | 2 +- doc/source/index.rst | 2 +- setup.py | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 2fc7323..4506810 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ cycler: composable cycles ========================= -Docs: http://matplotlib.org/cycler/ +Docs: https://matplotlib.org/cycler/ diff --git a/appveyor.yml b/appveyor.yml index 220fa3a..38e946f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ environment: global: # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the # /E:ON and /V:ON options are not enabled in the batch script intepreter - # See: http://stackoverflow.com/a/13751649/163740 + # See: https://stackoverflow.com/a/13751649/163740 CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci\\appveyor\\run_with_env.cmd" matrix: @@ -34,7 +34,7 @@ environment: PYTHON_ARCH: "64" install: - # Install Python (from the official .msi of http://python.org) and pip when + # Install Python (from the official .msi of https://python.org) and pip when # not already installed. - "powershell ./ci/appveyor/install.ps1" - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" diff --git a/ci/appveyor/install.ps1 b/ci/appveyor/install.ps1 index 0f165d8..e79a4f7 100644 --- a/ci/appveyor/install.ps1 +++ b/ci/appveyor/install.ps1 @@ -1,8 +1,8 @@ # Sample script to install Python and pip under Windows # Authors: Olivier Grisel, Jonathan Helmus and Kyle Kastner -# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ +# License: CC0 1.0 Universal: https://creativecommons.org/publicdomain/zero/1.0/ -$MINICONDA_URL = "http://repo.continuum.io/miniconda/" +$MINICONDA_URL = "https://repo.continuum.io/miniconda/" $BASE_URL = "https://www.python.org/ftp/python/" $GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" $GET_PIP_PATH = "C:\get-pip.py" diff --git a/ci/appveyor/run_with_env.cmd b/ci/appveyor/run_with_env.cmd index 0c70d63..72cc636 100644 --- a/ci/appveyor/run_with_env.cmd +++ b/ci/appveyor/run_with_env.cmd @@ -13,7 +13,7 @@ :: :: More details at: :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows -:: http://stackoverflow.com/a/13751649/163740 +:: https://stackoverflow.com/a/13751649/163740 :: :: Author: Olivier Grisel :: License: BSD 3 clause diff --git a/conda-recipe/bld.bat b/conda-recipe/bld.bat index 87b1481..2d84cc9 100644 --- a/conda-recipe/bld.bat +++ b/conda-recipe/bld.bat @@ -4,5 +4,5 @@ if errorlevel 1 exit 1 :: Add more build steps here, if they are necessary. :: See -:: http://docs.continuum.io/conda/build.html +:: https://conda.io/docs/user-guide/tasks/build-packages/index.html :: for a list of environment variables that are set during the build process. diff --git a/conda-recipe/build.sh b/conda-recipe/build.sh index 4d7fc03..4c6226d 100644 --- a/conda-recipe/build.sh +++ b/conda-recipe/build.sh @@ -5,5 +5,5 @@ $PYTHON setup.py install # Add more build steps here, if they are necessary. # See -# http://docs.continuum.io/conda/build.html +# https://conda.io/docs/user-guide/tasks/build-packages/index.html # for a list of environment variables that are set during the build process. diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index ea33deb..4adf9f5 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -54,10 +54,10 @@ test: # - nose about: - home: http://github.com/matplotlib/cycler + home: https://github.com/matplotlib/cycler license: BSD summary: 'Composable style cycles' # See -# http://docs.continuum.io/conda/build.html for +# https://conda.io/docs/building/build.html for # more information about meta.yaml diff --git a/doc/source/conf.py b/doc/source/conf.py index fb48a87..4ea2ca6 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -278,7 +278,7 @@ #texinfo_no_detailmenu = False intersphinx_mapping = {'python': ('https://docs.python.org/3.4', None), - 'matplotlb': ('http://matplotlib.org', None)} + 'matplotlb': ('https://matplotlib.org', None)} ################# numpydoc config #################### numpydoc_show_class_members = False diff --git a/doc/source/index.rst b/doc/source/index.rst index d584ebf..c93955e 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -11,7 +11,7 @@ ====== ==================================== -docs http://matplotlib.org/cycler +docs https://matplotlib.org/cycler pypi https://pypi.python.org/pypi/Cycler github https://github.com/matplotlib/cycler ====== ==================================== diff --git a/setup.py b/setup.py index 0cbff1c..3804e0d 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ author_email='matplotlib-users@python.org', py_modules=['cycler'], description='Composable style cycles', - url='http://github.com/matplotlib/cycler', + url='https://github.com/matplotlib/cycler', platforms='Cross platform (Linux, Mac OSX, Windows)', install_requires=['six'], license="BSD", From 19eef0e47ac75cbf2d0c7bdf76b40a21a69c0c8c Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 20 Dec 2017 15:27:20 +0200 Subject: [PATCH 08/98] Add badges --- README.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.rst b/README.rst index 4506810..38bce53 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,17 @@ +|PyPi|_ |Supported Python versions|_ |Travis|_ |Codecov|_ + +.. |PyPi| image:: https://img.shields.io/pypi/v/cycler.svg?style=flat +.. _PyPi: https://pypi.python.org/pypi/cycler + +.. |Supported Python versions| image:: https://img.shields.io/pypi/pyversions/cycler.svg +.. _Supported Python versions: https://pypi.python.org/pypi/cycler + +.. |Travis| image:: https://travis-ci.org/matplotlib/cycler.svg?branch=master +.. _Travis: https://travis-ci.org/matplotlib/cycler + +.. |Codecov| image:: https://codecov.io/github/matplotlib/cycler/badge.svg?branch=master&service=github +.. _Codecov: https://codecov.io/github/matplotlib/cycler?branch=master + cycler: composable cycles ========================= From 5b9113d935a9d2371e12d4cc42858bf50ca12036 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 15 Dec 2017 11:38:45 +0200 Subject: [PATCH 09/98] TST: Update Python versions --- .travis.yml | 1 + appveyor.yml | 20 ++++++++++++++------ setup.py | 3 ++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index b0669fd..8e839e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ matrix: - python: 2.7 - python: 3.4 - python: 3.5 + - python: 3.6 - python: "nightly" env: PRE=--pre allow_failures: diff --git a/appveyor.yml b/appveyor.yml index 38e946f..564526c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,27 +10,35 @@ environment: matrix: - PYTHON: "C:\\Python27_32" - PYTHON_VERSION: "2.7" + PYTHON_VERSION: "2.7.14" PYTHON_ARCH: "32" - PYTHON: "C:\\Python27_64" - PYTHON_VERSION: "2.7" + PYTHON_VERSION: "2.7.14" PYTHON_ARCH: "64" - PYTHON: "C:\\Python34_32" - PYTHON_VERSION: "3.4.3" + PYTHON_VERSION: "3.4.4" PYTHON_ARCH: "32" - PYTHON: "C:\\Python34_64" - PYTHON_VERSION: "3.4.3" + PYTHON_VERSION: "3.4.4" PYTHON_ARCH: "64" - PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5.0" + PYTHON_VERSION: "3.5.4" PYTHON_ARCH: "32" - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5.0" + PYTHON_VERSION: "3.5.4" + PYTHON_ARCH: "64" + + - PYTHON: "C:\\Python36" + PYTHON_VERSION: "3.6" + PYTHON_ARCH: "32" + + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6" PYTHON_ARCH: "64" install: diff --git a/setup.py b/setup.py index 3804e0d..7f11538 100644 --- a/setup.py +++ b/setup.py @@ -10,13 +10,14 @@ platforms='Cross platform (Linux, Mac OSX, Windows)', install_requires=['six'], license="BSD", + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', classifiers=['Development Status :: 4 - Beta', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], keywords='cycle kwargs', ) From 1c5099fb90ab3755bd7726ec5a19162b9f698f9c Mon Sep 17 00:00:00 2001 From: Nikita Kniazev Date: Wed, 20 Dec 2017 16:13:33 +0300 Subject: [PATCH 10/98] TST: Path consistency Also removes interference with appveyor python paths --- appveyor.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 564526c..e063f51 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -25,19 +25,19 @@ environment: PYTHON_VERSION: "3.4.4" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python35" + - PYTHON: "C:\\Python35_32" PYTHON_VERSION: "3.5.4" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python35-x64" + - PYTHON: "C:\\Python35_64" PYTHON_VERSION: "3.5.4" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python36" + - PYTHON: "C:\\Python36_32" PYTHON_VERSION: "3.6" PYTHON_ARCH: "32" - - PYTHON: "C:\\Python36-x64" + - PYTHON: "C:\\Python36_64" PYTHON_VERSION: "3.6" PYTHON_ARCH: "64" From 91412f895010e65c498a175a2898559dbd034b27 Mon Sep 17 00:00:00 2001 From: Nikita Kniazev Date: Wed, 20 Dec 2017 19:22:12 +0300 Subject: [PATCH 11/98] TST: Use already installed in Appveyor Python --- appveyor.yml | 42 ++-------- ci/appveyor/install.ps1 | 180 ---------------------------------------- 2 files changed, 8 insertions(+), 214 deletions(-) delete mode 100644 ci/appveyor/install.ps1 diff --git a/appveyor.yml b/appveyor.yml index e063f51..6720de8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,42 +9,16 @@ environment: CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci\\appveyor\\run_with_env.cmd" matrix: - - PYTHON: "C:\\Python27_32" - PYTHON_VERSION: "2.7.14" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python27_64" - PYTHON_VERSION: "2.7.14" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python34_32" - PYTHON_VERSION: "3.4.4" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python34_64" - PYTHON_VERSION: "3.4.4" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python35_32" - PYTHON_VERSION: "3.5.4" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python35_64" - PYTHON_VERSION: "3.5.4" - PYTHON_ARCH: "64" - - - PYTHON: "C:\\Python36_32" - PYTHON_VERSION: "3.6" - PYTHON_ARCH: "32" - - - PYTHON: "C:\\Python36_64" - PYTHON_VERSION: "3.6" - PYTHON_ARCH: "64" + - PYTHON: "C:\\Python27" + - PYTHON: "C:\\Python27-x64" + - PYTHON: "C:\\Python34" + - PYTHON: "C:\\Python34-x64" + - PYTHON: "C:\\Python35" + - PYTHON: "C:\\Python35-x64" + - PYTHON: "C:\\Python36" + - PYTHON: "C:\\Python36-x64" install: - # Install Python (from the official .msi of https://python.org) and pip when - # not already installed. - - "powershell ./ci/appveyor/install.ps1" - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" # Check that we have the expected version and architecture for Python diff --git a/ci/appveyor/install.ps1 b/ci/appveyor/install.ps1 deleted file mode 100644 index e79a4f7..0000000 --- a/ci/appveyor/install.ps1 +++ /dev/null @@ -1,180 +0,0 @@ -# Sample script to install Python and pip under Windows -# Authors: Olivier Grisel, Jonathan Helmus and Kyle Kastner -# License: CC0 1.0 Universal: https://creativecommons.org/publicdomain/zero/1.0/ - -$MINICONDA_URL = "https://repo.continuum.io/miniconda/" -$BASE_URL = "https://www.python.org/ftp/python/" -$GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" -$GET_PIP_PATH = "C:\get-pip.py" - - -function DownloadPython ($python_version, $platform_suffix) { - $webclient = New-Object System.Net.WebClient - $filename = "python-" + $python_version + $platform_suffix + ".msi" - $url = $BASE_URL + $python_version + "/" + $filename - - $basedir = $pwd.Path + "\" - $filepath = $basedir + $filename - if (Test-Path $filename) { - Write-Host "Reusing" $filepath - return $filepath - } - - # Download and retry up to 3 times in case of network transient errors. - Write-Host "Downloading" $filename "from" $url - $retry_attempts = 2 - for($i=0; $i -lt $retry_attempts; $i++){ - try { - $webclient.DownloadFile($url, $filepath) - break - } - Catch [Exception]{ - Start-Sleep 1 - } - } - if (Test-Path $filepath) { - Write-Host "File saved at" $filepath - } else { - # Retry once to get the error message if any at the last try - $webclient.DownloadFile($url, $filepath) - } - return $filepath -} - - -function InstallPython ($python_version, $architecture, $python_home) { - Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home - if (Test-Path $python_home) { - Write-Host $python_home "already exists, skipping." - return $false - } - if ($architecture -eq "32") { - $platform_suffix = "" - } else { - $platform_suffix = ".amd64" - } - $msipath = DownloadPython $python_version $platform_suffix - Write-Host "Installing" $msipath "to" $python_home - $install_log = $python_home + ".log" - $install_args = "/qn /log $install_log /i $msipath TARGETDIR=$python_home" - $uninstall_args = "/qn /x $msipath" - RunCommand "msiexec.exe" $install_args - if (-not(Test-Path $python_home)) { - Write-Host "Python seems to be installed else-where, reinstalling." - RunCommand "msiexec.exe" $uninstall_args - RunCommand "msiexec.exe" $install_args - } - if (Test-Path $python_home) { - Write-Host "Python $python_version ($architecture) installation complete" - } else { - Write-Host "Failed to install Python in $python_home" - Get-Content -Path $install_log - Exit 1 - } -} - -function RunCommand ($command, $command_args) { - Write-Host $command $command_args - Start-Process -FilePath $command -ArgumentList $command_args -Wait -Passthru -} - - -function InstallPip ($python_home) { - $pip_path = $python_home + "\Scripts\pip.exe" - $python_path = $python_home + "\python.exe" - if (-not(Test-Path $pip_path)) { - Write-Host "Installing pip..." - $webclient = New-Object System.Net.WebClient - $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) - Write-Host "Executing:" $python_path $GET_PIP_PATH - Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru - } else { - Write-Host "pip already installed." - } -} - - -function DownloadMiniconda ($python_version, $platform_suffix) { - $webclient = New-Object System.Net.WebClient - if ($python_version -eq "3.4") { - $filename = "Miniconda3-3.5.5-Windows-" + $platform_suffix + ".exe" - } else { - $filename = "Miniconda-3.5.5-Windows-" + $platform_suffix + ".exe" - } - $url = $MINICONDA_URL + $filename - - $basedir = $pwd.Path + "\" - $filepath = $basedir + $filename - if (Test-Path $filename) { - Write-Host "Reusing" $filepath - return $filepath - } - - # Download and retry up to 3 times in case of network transient errors. - Write-Host "Downloading" $filename "from" $url - $retry_attempts = 2 - for($i=0; $i -lt $retry_attempts; $i++){ - try { - $webclient.DownloadFile($url, $filepath) - break - } - Catch [Exception]{ - Start-Sleep 1 - } - } - if (Test-Path $filepath) { - Write-Host "File saved at" $filepath - } else { - # Retry once to get the error message if any at the last try - $webclient.DownloadFile($url, $filepath) - } - return $filepath -} - - -function InstallMiniconda ($python_version, $architecture, $python_home) { - Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home - if (Test-Path $python_home) { - Write-Host $python_home "already exists, skipping." - return $false - } - if ($architecture -eq "32") { - $platform_suffix = "x86" - } else { - $platform_suffix = "x86_64" - } - $filepath = DownloadMiniconda $python_version $platform_suffix - Write-Host "Installing" $filepath "to" $python_home - $install_log = $python_home + ".log" - $args = "/S /D=$python_home" - Write-Host $filepath $args - Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru - if (Test-Path $python_home) { - Write-Host "Python $python_version ($architecture) installation complete" - } else { - Write-Host "Failed to install Python in $python_home" - Get-Content -Path $install_log - Exit 1 - } -} - - -function InstallMinicondaPip ($python_home) { - $pip_path = $python_home + "\Scripts\pip.exe" - $conda_path = $python_home + "\Scripts\conda.exe" - if (-not(Test-Path $pip_path)) { - Write-Host "Installing pip..." - $args = "install --yes pip" - Write-Host $conda_path $args - Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru - } else { - Write-Host "pip already installed." - } -} - -function main () { - InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON - InstallPip $env:PYTHON -} - -main From 5dc55d88e1c60e28e4b51842f01061433ef295fa Mon Sep 17 00:00:00 2001 From: Nikita Kniazev Date: Wed, 20 Dec 2017 19:22:59 +0300 Subject: [PATCH 12/98] TST: Drop run_with_env.cmd --- appveyor.yml | 8 +----- ci/appveyor/run_with_env.cmd | 47 ------------------------------------ 2 files changed, 1 insertion(+), 54 deletions(-) delete mode 100644 ci/appveyor/run_with_env.cmd diff --git a/appveyor.yml b/appveyor.yml index 6720de8..c514f2e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,12 +2,6 @@ # Windows environment: - global: - # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the - # /E:ON and /V:ON options are not enabled in the batch script intepreter - # See: https://stackoverflow.com/a/13751649/163740 - CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci\\appveyor\\run_with_env.cmd" - matrix: - PYTHON: "C:\\Python27" - PYTHON: "C:\\Python27-x64" @@ -26,7 +20,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 pytest pytest-cov coverage" + - "pip install -v six nose pytest pytest-cov coverage" # Install the generated wheel package to test it - "python setup.py install" diff --git a/ci/appveyor/run_with_env.cmd b/ci/appveyor/run_with_env.cmd deleted file mode 100644 index 72cc636..0000000 --- a/ci/appveyor/run_with_env.cmd +++ /dev/null @@ -1,47 +0,0 @@ -:: To build extensions for 64 bit Python 3, we need to configure environment -:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) -:: -:: To build extensions for 64 bit Python 2, we need to configure environment -:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: -:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) -:: -:: 32 bit builds do not require specific environment configurations. -:: -:: Note: this script needs to be run with the /E:ON and /V:ON flags for the -:: cmd interpreter, at least for (SDK v7.0) -:: -:: More details at: -:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows -:: https://stackoverflow.com/a/13751649/163740 -:: -:: Author: Olivier Grisel -:: License: BSD 3 clause -@ECHO OFF - -SET COMMAND_TO_RUN=%* -SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows - -SET MAJOR_PYTHON_VERSION="%PYTHON_VERSION:~0,1%" -IF %MAJOR_PYTHON_VERSION% == "2" ( - SET WINDOWS_SDK_VERSION="v7.0" -) ELSE IF %MAJOR_PYTHON_VERSION% == "3" ( - SET WINDOWS_SDK_VERSION="v7.1" -) ELSE ( - ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" - EXIT 1 -) - -IF "%PYTHON_ARCH%"=="64" ( - ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture - SET DISTUTILS_USE_SDK=1 - SET MSSdk=1 - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% - "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 -) ELSE ( - ECHO Using default MSVC build environment for 32 bit architecture - ECHO Executing: %COMMAND_TO_RUN% - call %COMMAND_TO_RUN% || EXIT 1 -) From 408223a6d0663e6f28e128390d7dd7864d9a604f Mon Sep 17 00:00:00 2001 From: Todd Date: Sat, 9 Jun 2018 23:21:10 -0400 Subject: [PATCH 13/98] Include LICENSE file in wheels The license requires that all copies of the software include the license. This makes sure the license is included in the wheels. See the wheel documentation [here](https://wheel.readthedocs.io/en/stable/#including-the-license-in-the-generated-wheel-file) for more information. --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0c9e0fc --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +license_file = LICENSE From 5fce18ade0ed393111d61272fbaf0df5fb7f4aeb Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 28 Aug 2018 11:13:34 +0200 Subject: [PATCH 14/98] Remove dependency on six. Mostly so that Matplotlib doesn't depend *transitively* on six either :) - six.iteritems() -> dict.items(): OK even on Py2, we directly consume the iterator/list with reduce anyways, and I doubt that the perfomance change is measurable. - six.moves.zip() could have been replaced with builtin zip() on both Py2 and Py3 because we always exhaust the iterator immediately anyways, but it also acts as a lookup key in `Cycler.__len__` so I decided to just play it safe and have a conditional import (on Py2, six.moves.zip is an alias for itertools.zip). Also added new .pytest_cache directory to gitignore. --- .gitignore | 1 + .travis.yml | 2 +- appveyor.yml | 2 +- conda-recipe/meta.yaml | 2 -- cycler.py | 20 ++++++++++---------- setup.py | 1 - test_cycler.py | 20 +++++++++++++------- 7 files changed, 26 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index be42995..20908de 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ htmlcov/ cover/ .coverage .cache +.pytest_cache nosetests.xml coverage.xml cover/ diff --git a/.travis.yml b/.travis.yml index 8e839e3..acde10e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ matrix: install: - python setup.py install - - pip install pytest pytest-cov coverage six + - pip install pytest pytest-cov coverage script: - coverage run run_tests.py diff --git a/appveyor.yml b/appveyor.yml index c514f2e..12c23c7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,7 +20,7 @@ install: - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # Install the build and runtime dependencies of the project. - - "pip install -v six nose pytest pytest-cov coverage" + - "pip install -v nose pytest pytest-cov coverage" # Install the generated wheel package to test it - "python setup.py install" diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 4adf9f5..cab179e 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -30,11 +30,9 @@ requirements: build: - python - setuptools - - six run: - python - - six test: # Python imports diff --git a/cycler.py b/cycler.py index 8400444..3b05dc1 100644 --- a/cycler.py +++ b/cycler.py @@ -43,11 +43,14 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) -import six +import copy +from functools import reduce from itertools import product, cycle -from six.moves import zip, reduce from operator import mul, add -import copy +import sys + +if sys.version_info < (3,): + from itertools import izip as zip __version__ = '0.10.0' @@ -183,7 +186,6 @@ def change_key(self, old, new): def _compose(self): """ Compose the 'left' and 'right' components of this cycle - with the proper operation (zip or product as of now) """ for a, b in self._op(self._left, self._right): out = dict() @@ -220,8 +222,7 @@ def __getitem__(self, key): # TODO : maybe add numpy style fancy slicing if isinstance(key, slice): trans = self.by_key() - return reduce(add, (_cycler(k, v[key]) - for k, v in six.iteritems(trans))) + return reduce(add, (_cycler(k, v[key]) for k, v in trans.items())) else: raise ValueError("Can only use slices with Cycler.__getitem__") @@ -259,8 +260,7 @@ def __mul__(self, other): return Cycler(self, other, product) elif isinstance(other, int): trans = self.by_key() - return reduce(add, (_cycler(k, v*other) - for k, v in six.iteritems(trans))) + return reduce(add, (_cycler(k, v*other) for k, v in trans.items())) else: return NotImplemented @@ -396,7 +396,7 @@ def simplify(self): # I would believe that there is some performance implications trans = self.by_key() - return reduce(add, (_cycler(k, v) for k, v in six.iteritems(trans))) + return reduce(add, (_cycler(k, v) for k, v in trans.items())) def concat(self, other): """Concatenate this cycler and an other. @@ -523,7 +523,7 @@ def cycler(*args, **kwargs): "positional argument. Use keyword arguments instead.") if kwargs: - return reduce(add, (_cycler(k, v) for k, v in six.iteritems(kwargs))) + return reduce(add, (_cycler(k, v) for k, v in kwargs.items())) raise TypeError("Must have at least a positional OR keyword arguments") diff --git a/setup.py b/setup.py index 7f11538..fc2d777 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,6 @@ description='Composable style cycles', url='https://github.com/matplotlib/cycler', platforms='Cross platform (Linux, Mac OSX, Windows)', - install_requires=['six'], license="BSD", python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', classifiers=['Development Status :: 4 - Beta', diff --git a/test_cycler.py b/test_cycler.py index 52f65ec..50c17f0 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -1,12 +1,18 @@ from __future__ import (absolute_import, division, print_function) -import six -from six.moves import zip, range -from cycler import cycler, Cycler, concat -import pytest -from itertools import product, cycle, chain -from operator import add, iadd, mul, imul from collections import defaultdict +from operator import add, iadd, mul, imul +from itertools import product, cycle, chain +import sys + +import pytest + +from cycler import cycler, Cycler, concat + +if sys.version_info < (3,): + from itertools import izip as zip + range = xrange + str = unicode def _cycler_helper(c, length, keys, values): @@ -147,7 +153,7 @@ def test_fail_getime(): def _repr_tester_helper(rpr_func, cyc, target_repr): test_repr = getattr(cyc, rpr_func)() - assert six.text_type(test_repr) == six.text_type(target_repr) + assert str(test_repr) == str(target_repr) def test_repr(): From 5dfba113dcb984ae3eabe2e6556f3f19fca4e9d2 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 18 May 2019 20:47:40 -0400 Subject: [PATCH 15/98] MNT: fill out the data model --- cycler.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cycler.py b/cycler.py index 3b05dc1..f6245c1 100644 --- a/cycler.py +++ b/cycler.py @@ -324,6 +324,11 @@ def __eq__(self, other): return all(a == b for a, b in zip(self, other)) + def __ne__(self, other): + return not (self == other) + + __hash__ = None + def __repr__(self): op_map = {zip: '+', product: '*'} if self._right is None: From e569fd96d4278e4f5908951496fc69904ea8bc06 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 18 May 2019 21:06:29 -0400 Subject: [PATCH 16/98] TST: parameterize test_creation --- test_cycler.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test_cycler.py b/test_cycler.py index 50c17f0..55ea27b 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -28,13 +28,14 @@ def _cycles_equal(c1, c2): assert list(c1) == list(c2) -def test_creation(): - c = cycler(c='rgb') - yield _cycler_helper, c, 3, ['c'], [['r', 'g', 'b']] - c = cycler(c=list('rgb')) - yield _cycler_helper, c, 3, ['c'], [['r', 'g', 'b']] - c = cycler(cycler(c='rgb')) - yield _cycler_helper, c, 3, ['c'], [['r', 'g', 'b']] +@pytest.mark.parametrize('c', [cycler(c='rgb'), + cycler(c=list('rgb')), + cycler(cycler(c='rgb'))], + ids=['from string', + 'from list', + 'from cycler']) +def test_creation(c): + _cycler_helper(c, 3, ['c'], [['r', 'g', 'b']]) def test_compose(): From 58606959261618083950cdbafc01c93d7f4abd17 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 18 May 2019 21:58:15 -0400 Subject: [PATCH 17/98] TST: make testing helper a bit more paranoid --- test_cycler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test_cycler.py b/test_cycler.py index 55ea27b..9829226 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -26,6 +26,7 @@ def _cycler_helper(c, length, keys, values): def _cycles_equal(c1, c2): assert list(c1) == list(c2) + assert c1 == c2 @pytest.mark.parametrize('c', [cycler(c='rgb'), From 0d75afa5f9d93b0c4dd483661414cff9aa8d989f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 18 May 2019 21:58:37 -0400 Subject: [PATCH 18/98] TST: remove 'yield' generated tests pytest no longer supports them. Did this by just calling the helper functions in-line. It does make the tests bigger, but this changes the testing code in the minimal way possible. --- test_cycler.py | 89 +++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/test_cycler.py b/test_cycler.py index 9829226..ded15c0 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -39,62 +39,71 @@ def test_creation(c): _cycler_helper(c, 3, ['c'], [['r', 'g', 'b']]) -def test_compose(): +def test_add(): c1 = cycler(c='rgb') c2 = cycler(lw=range(3)) - c3 = cycler(lw=range(15)) # addition - yield _cycler_helper, c1+c2, 3, ['c', 'lw'], [list('rgb'), range(3)] - yield _cycler_helper, c2+c1, 3, ['c', 'lw'], [list('rgb'), range(3)] - yield _cycles_equal, c2+c1, c1+c2 + _cycler_helper(c1+c2, 3, ['c', 'lw'], [list('rgb'), range(3)]) + _cycler_helper(c2+c1, 3, ['c', 'lw'], [list('rgb'), range(3)]) + _cycles_equal(c2+c1, c1+c2) + + +def test_add_len_mismatch(): # miss-matched add lengths + c1 = cycler(c='rgb') + c3 = cycler(lw=range(15)) with pytest.raises(ValueError): c1 + c3 with pytest.raises(ValueError): c3 + c1 + +def test_prod(): + c1 = cycler(c='rgb') + c2 = cycler(lw=range(3)) + c3 = cycler(lw=range(15)) # multiplication target = zip(*product(list('rgb'), range(3))) - yield (_cycler_helper, c1 * c2, 9, ['c', 'lw'], target) + _cycler_helper(c1 * c2, 9, ['c', 'lw'], target) target = zip(*product(range(3), list('rgb'))) - yield (_cycler_helper, c2 * c1, 9, ['lw', 'c'], target) + _cycler_helper(c2 * c1, 9, ['lw', 'c'], target) target = zip(*product(range(15), list('rgb'))) - yield (_cycler_helper, c3 * c1, 45, ['lw', 'c'], target) + _cycler_helper(c3 * c1, 45, ['lw', 'c'], target) def test_inplace(): c1 = cycler(c='rgb') c2 = cycler(lw=range(3)) c2 += c1 - yield _cycler_helper, c2, 3, ['c', 'lw'], [list('rgb'), range(3)] + _cycler_helper(c2, 3, ['c', 'lw'], [list('rgb'), range(3)]) c3 = cycler(c='rgb') c4 = cycler(lw=range(3)) c3 *= c4 target = zip(*product(list('rgb'), range(3))) - yield (_cycler_helper, c3, 9, ['c', 'lw'], target) + _cycler_helper(c3, 9, ['c', 'lw'], target) def test_constructor(): c1 = cycler(c='rgb') c2 = cycler(ec=c1) - yield _cycler_helper, c1+c2, 3, ['c', 'ec'], [['r', 'g', 'b']]*2 + _cycler_helper(c1+c2, 3, ['c', 'ec'], [['r', 'g', 'b']]*2) c3 = cycler(c=c1) - yield _cycler_helper, c3+c2, 3, ['c', 'ec'], [['r', 'g', 'b']]*2 + _cycler_helper(c3+c2, 3, ['c', 'ec'], [['r', 'g', 'b']]*2) # Using a non-string hashable c4 = cycler(1, range(3)) - yield _cycler_helper, c4+c1, 3, [1, 'c'], [range(3), ['r', 'g', 'b']] + _cycler_helper(c4+c1, 3, [1, 'c'], [range(3), ['r', 'g', 'b']]) # addition using cycler() - yield (_cycler_helper, cycler(c='rgb', lw=range(3)), - 3, ['c', 'lw'], [list('rgb'), range(3)]) - yield (_cycler_helper, cycler(lw=range(3), c='rgb'), - 3, ['c', 'lw'], [list('rgb'), range(3)]) + _cycler_helper(cycler(c='rgb', lw=range(3)), + 3, ['c', 'lw'], [list('rgb'), range(3)]) + _cycler_helper(cycler(lw=range(3), c='rgb'), + 3, ['c', 'lw'], [list('rgb'), range(3)]) # Purposely mixing them - yield (_cycler_helper, cycler(c=range(3), lw=c1), - 3, ['c', 'lw'], [range(3), list('rgb')]) + _cycler_helper(cycler(c=range(3), lw=c1), + 3, ['c', 'lw'], [range(3), list('rgb')]) def test_failures(): @@ -116,24 +125,24 @@ def test_simplify(): c1 = cycler(c='rgb') c2 = cycler(ec=c1) for c in [c1 * c2, c2 * c1, c1 + c2]: - yield _cycles_equal, c, c.simplify() + _cycles_equal(c, c.simplify()) def test_multiply(): c1 = cycler(c='rgb') - yield _cycler_helper, 2*c1, 6, ['c'], ['rgb'*2] + _cycler_helper(2*c1, 6, ['c'], ['rgb'*2]) c2 = cycler(ec=c1) c3 = c1 * c2 - yield _cycles_equal, 2*c3, c3*2 + _cycles_equal(2*c3, c3*2) def test_mul_fails(): c1 = cycler(c='rgb') - pytest.raises(TypeError, mul, c1, 2.0) - pytest.raises(TypeError, mul, c1, 'a') - pytest.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(): @@ -143,7 +152,7 @@ def test_getitem(): slice(None, None, -1), slice(1, 5, None), slice(0, 5, 2)): - yield _cycles_equal, c1[slc], cycler(3, widths[slc]) + _cycles_equal(c1[slc], cycler(3, widths[slc])) def test_fail_getime(): @@ -166,14 +175,14 @@ def test_repr(): c_sum_rpr = "(cycler('c', ['r', 'g', 'b']) + cycler('3rd', [0, 1, 2]))" c_prod_rpr = "(cycler('c', ['r', 'g', 'b']) * cycler('3rd', [0, 1, 2]))" - yield _repr_tester_helper, '__repr__', c + c2, c_sum_rpr - yield _repr_tester_helper, '__repr__', c * c2, c_prod_rpr + _repr_tester_helper('__repr__', c + c2, c_sum_rpr) + _repr_tester_helper('__repr__', c * c2, c_prod_rpr) sum_html = "
'3rd''c'
0'r'
1'g'
2'b'
" prod_html = "
'3rd''c'
0'r'
1'r'
2'r'
0'g'
1'g'
2'g'
0'b'
1'b'
2'b'
" - yield _repr_tester_helper, '_repr_html_', c + c2, sum_html - yield _repr_tester_helper, '_repr_html_', c * c2, prod_html + _repr_tester_helper('_repr_html_', c + c2, sum_html) + _repr_tester_helper('_repr_html_', c * c2, prod_html) def test_call(): @@ -266,17 +275,17 @@ def _eq_test_helper(a, b, res): def test_eq(): a = cycler(c='rgb') b = cycler(c='rgb') - yield _eq_test_helper, a, b, True - yield _eq_test_helper, a, b[::-1], False + _eq_test_helper(a, b, True) + _eq_test_helper(a, b[::-1], False) c = cycler(lw=range(3)) - yield _eq_test_helper, a+c, c+a, True - yield _eq_test_helper, a+c, c+b, True - yield _eq_test_helper, a*c, c*a, False - yield _eq_test_helper, a, c, False + _eq_test_helper(a+c, c+a, True) + _eq_test_helper(a+c, c+b, True) + _eq_test_helper(a*c, c*a, False) + _eq_test_helper(a, c, False) d = cycler(c='ymk') - yield _eq_test_helper, b, d, False + _eq_test_helper(b, d, False) e = cycler(c='orange') - yield _eq_test_helper, b, e, False + _eq_test_helper(b, e, False) def test_cycler_exceptions(): @@ -325,7 +334,7 @@ def test_by_key_add(): cy = cycler(c=input_dict['c']) + cycler(lw=input_dict['lw']) res = cy.by_key() assert res == input_dict - yield _by_key_helper, cy + _by_key_helper(cy) def test_by_key_mul(): @@ -333,7 +342,7 @@ def test_by_key_mul(): cy = cycler(c=input_dict['c']) * cycler(lw=input_dict['lw']) res = cy.by_key() assert input_dict['lw'] * len(input_dict['c']) == res['lw'] - yield _by_key_helper, cy + _by_key_helper(cy) def test_contains(): From 5e568f593043e0b9f9e838ebe5859516c8d013c5 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 13 Sep 2019 18:01:39 -0400 Subject: [PATCH 19/98] CI: add py37 to matrix --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index acde10e..29b9ea4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ matrix: - python: 3.4 - python: 3.5 - python: 3.6 + - python: 3.7 - python: "nightly" env: PRE=--pre allow_failures: From 79eca30044cdbad700f9c5a98a3a066873b04d56 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 13 Sep 2019 18:02:34 -0400 Subject: [PATCH 20/98] CI: actually use PRE env let pip upgrade dependencies --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 29b9ea4..15f10d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,8 +13,9 @@ matrix: - python : "nightly" install: + - pip install --upgrade pip - python setup.py install - - pip install pytest pytest-cov coverage + - pip install $PRE --upgrade pytest pytest-cov coverage script: - coverage run run_tests.py From 5efeffd8a1837a1ccbad680455639f8492f6073a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 13 Sep 2019 18:03:19 -0400 Subject: [PATCH 21/98] CI: install our self via pip not python setup.py --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 15f10d9..fa0089a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ matrix: install: - pip install --upgrade pip - - python setup.py install + - pip install -v . - pip install $PRE --upgrade pytest pytest-cov coverage script: From f28a4070304d5daa02f4f1ba9f25cf52cdd59eef Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 13 Sep 2019 18:03:47 -0400 Subject: [PATCH 22/98] CI: don't use bare `pip`, use `python -m pip` instead --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index fa0089a..9b41696 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,9 +13,9 @@ matrix: - python : "nightly" install: - - pip install --upgrade pip - - pip install -v . - - pip install $PRE --upgrade pytest pytest-cov coverage + - python -m pip install --upgrade pip + - python -m pip install -v . + - python -m pip install $PRE --upgrade pytest pytest-cov coverage script: - coverage run run_tests.py From ff9cd17f7399ed75d0395485aeae1a9d874a3817 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 18 Feb 2020 00:49:28 -0500 Subject: [PATCH 23/98] DOC: stop using deprecated sphinx extensions --- doc/source/conf.py | 1 - doc/source/index.rst | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 4ea2ca6..8f88342 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -36,7 +36,6 @@ 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', 'sphinx.ext.autosummary', - 'matplotlib.sphinxext.only_directives', 'matplotlib.sphinxext.plot_directive', 'IPython.sphinxext.ipython_directive', 'IPython.sphinxext.ipython_console_highlighting', diff --git a/doc/source/index.rst b/doc/source/index.rst index c93955e..5f8f884 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -4,7 +4,7 @@ Composable cycles =================== -.. htmlonly:: +.. only:: html :Version: |version| :Date: |today| From d5682e4729f690a7b166dbcf371d03f0169bdd57 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 18 Feb 2020 00:49:45 -0500 Subject: [PATCH 24/98] DOC: remove whitespace that is breaking IPython --- doc/source/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 5f8f884..518fb39 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -50,7 +50,6 @@ hashable (as it will eventually be used as the key in a :obj:`dict`). from __future__ import print_function from cycler import cycler - color_cycle = cycler(color=['r', 'g', 'b']) color_cycle From 1bb5e8b80112e43212cc25d34c25ffeca5387b94 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 18 Feb 2020 00:54:52 -0500 Subject: [PATCH 25/98] DOC: update for sphinx >= 1.8 --- doc/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 8f88342..2a4c3bd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -46,7 +46,7 @@ autosummary_generate = True numpydoc_show_class_members = False -autodoc_default_flags = ['members'] +autodoc_default_options = {'members': True} # Add any paths that contain templates here, relative to this directory. From 71ea5cdc612db5ba4ee521f429d5e40d6761b48f Mon Sep 17 00:00:00 2001 From: johnthagen Date: Wed, 16 Dec 2020 12:18:38 -0500 Subject: [PATCH 26/98] Add license trove classifier --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fc2d777..8b50257 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,8 @@ platforms='Cross platform (Linux, Mac OSX, Windows)', license="BSD", python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', - classifiers=['Development Status :: 4 - Beta', + classifiers=['License :: OSI Approved :: BSD License', + 'Development Status :: 4 - Beta', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', From 4620d5cb58fc94d602b718fcd4d421745c7d72f1 Mon Sep 17 00:00:00 2001 From: johnthagen Date: Wed, 16 Dec 2020 12:32:49 -0500 Subject: [PATCH 27/98] Remove extra space in error message --- cycler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cycler.py b/cycler.py index f6245c1..1cd4332 100644 --- a/cycler.py +++ b/cycler.py @@ -519,7 +519,7 @@ def cycler(*args, **kwargs): if len(args) == 1: if not isinstance(args[0], Cycler): raise TypeError("If only one positional argument given, it must " - " be a Cycler instance.") + "be a Cycler instance.") return Cycler(args[0]) elif len(args) == 2: return _cycler(*args) From 3fa066d67ec382e0544988b5349e2cbfb87b7e99 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 9 Feb 2021 11:50:52 +0100 Subject: [PATCH 28/98] Don't dupe concat() definition; inline _compose(). The `concat` method can just directly reuse the free function definition. (Also using string-joining was a bit overkill.) Inlining `_compose` into `__iter__` (its sole user) seems easier to follow than having it defined nearly 50 lines away). --- cycler.py | 124 +++++++++++++++++++----------------------------------- 1 file changed, 44 insertions(+), 80 deletions(-) diff --git a/cycler.py b/cycler.py index 1cd4332..bb0ab4d 100644 --- a/cycler.py +++ b/cycler.py @@ -77,6 +77,41 @@ def _process_keys(left, right): return l_key | r_key +def concat(left, right): + """ + Concatenate two cyclers, as if chained using `itertools.chain`. + + The keys must match exactly. + + Examples + -------- + + >>> num = cycler('a', range(3)) + >>> let = cycler('a', 'abc') + >>> num.concat(let) + cycler('a', [0, 1, 2, 'a', 'b', 'c']) + + Parameters + ---------- + left, right : `Cycler` + The two `Cycler` instances to concatenate + + Returns + ------- + ret : `Cycler` + The concatenated `Cycler` + """ + if left.keys != right.keys: + raise ValueError("Keys do not match:\n" + "\tIntersection: {both!r}\n" + "\tDisjoint: {just_one!r}".format( + both=left.keys & right.keys, + just_one=left.keys ^ right.keys)) + _l = left.by_key() + _r = right.by_key() + return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys)) + + class Cycler(object): """ Composable cycles @@ -183,16 +218,6 @@ def change_key(self, old, new): # iteration. self._left = [{new: entry[old]} for entry in self._left] - def _compose(self): - """ - Compose the 'left' and 'right' components of this cycle - """ - for a, b in self._op(self._left, self._right): - out = dict() - out.update(a) - out.update(b) - yield out - @classmethod def _from_iter(cls, label, itr): """ @@ -228,9 +253,14 @@ def __getitem__(self, key): def __iter__(self): if self._right is None: - return iter(dict(l) for l in self._left) - - return self._compose() + for l in self._left: + yield dict(l) + else: + for a, b in self._op(self._left, self._right): + out = dict() + out.update(a) + out.update(b) + yield out def __add__(self, other): """ @@ -403,73 +433,7 @@ def simplify(self): trans = self.by_key() return reduce(add, (_cycler(k, v) for k, v in trans.items())) - def concat(self, other): - """Concatenate this cycler and an other. - - The keys must match exactly. - - This returns a single Cycler which is equivalent to - `itertools.chain(self, other)` - - Examples - -------- - - >>> num = cycler('a', range(3)) - >>> let = cycler('a', 'abc') - >>> num.concat(let) - cycler('a', [0, 1, 2, 'a', 'b', 'c']) - - Parameters - ---------- - other : `Cycler` - The `Cycler` to concatenate to this one. - - Returns - ------- - ret : `Cycler` - The concatenated `Cycler` - """ - return concat(self, other) - - -def concat(left, right): - """Concatenate two cyclers. - - The keys must match exactly. - - This returns a single Cycler which is equivalent to - `itertools.chain(left, right)` - - Examples - -------- - - >>> num = cycler('a', range(3)) - >>> let = cycler('a', 'abc') - >>> num.concat(let) - cycler('a', [0, 1, 2, 'a', 'b', 'c']) - - Parameters - ---------- - left, right : `Cycler` - The two `Cycler` instances to concatenate - - Returns - ------- - ret : `Cycler` - The concatenated `Cycler` - """ - if left.keys != right.keys: - msg = '\n\t'.join(["Keys do not match:", - "Intersection: {both!r}", - "Disjoint: {just_one!r}"]).format( - both=left.keys & right.keys, - just_one=left.keys ^ right.keys) - - raise ValueError(msg) - - _l = left.by_key() - _r = right.by_key() - return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys)) + concat = concat def cycler(*args, **kwargs): From 0429ea2968da3098f653d3af4979909ad4b02010 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 9 Feb 2021 12:01:42 +0100 Subject: [PATCH 29/98] Minor doc cleanups. Punctuation. Use "cycler" instead of "Cycler" or "cycle" in sentences slightly more consistently. Remove parameter descriptions that add no information ("the second Cycler"). Remove unnecessary whitespace. --- cycler.py | 80 +++++++++++++++++++------------------------------------ 1 file changed, 28 insertions(+), 52 deletions(-) diff --git a/cycler.py b/cycler.py index bb0ab4d..f31fe85 100644 --- a/cycler.py +++ b/cycler.py @@ -57,16 +57,17 @@ def _process_keys(left, right): """ - Helper function to compose cycler keys + Helper function to compose cycler keys. Parameters ---------- left, right : iterable of dictionaries or None - The cyclers to be composed + The cyclers to be composed. + Returns ------- keys : set - The keys in the composition of the two cyclers + The keys in the composition of the two cyclers. """ l_peek = next(iter(left)) if left is not None else {} r_peek = next(iter(right)) if right is not None else {} @@ -78,28 +79,22 @@ def _process_keys(left, right): def concat(left, right): - """ - Concatenate two cyclers, as if chained using `itertools.chain`. + r""" + Concatenate `Cycler`\s, as if chained using `itertools.chain`. The keys must match exactly. Examples -------- - >>> num = cycler('a', range(3)) >>> let = cycler('a', 'abc') >>> num.concat(let) cycler('a', [0, 1, 2, 'a', 'b', 'c']) - Parameters - ---------- - left, right : `Cycler` - The two `Cycler` instances to concatenate - Returns ------- - ret : `Cycler` - The concatenated `Cycler` + `Cycler` + The concatenated cycler. """ if left.keys != right.keys: raise ValueError("Keys do not match:\n" @@ -114,7 +109,7 @@ def concat(left, right): class Cycler(object): """ - Composable cycles + Composable cycles. This class has compositions methods: @@ -130,25 +125,22 @@ class Cycler(object): ``*=`` in-place ``*`` - and supports basic slicing via ``[]`` + and supports basic slicing via ``[]``. Parameters ---------- - left : Cycler or None - The 'left' cycler - - right : Cycler or None - The 'right' cycler - + left, right : Cycler or None + The 'left' and 'right' cyclers. op : func or None Function which composes the 'left' and 'right' cyclers. - """ + def __call__(self): return cycle(self) def __init__(self, left, right=None, op=None): - """Semi-private init + """ + Semi-private init. Do not use this directly, use `cycler` function instead. """ @@ -178,9 +170,7 @@ def __contains__(self, k): @property def keys(self): - """ - The keys this Cycler knows about - """ + """The keys this Cycler knows about.""" return set(self._keys) def change_key(self, old, new): @@ -191,7 +181,6 @@ def change_key(self, old, new): Does nothing if the old key is the same as the new key. Raises a ValueError if the new key is already a key. Raises a KeyError if the old key isn't a key. - """ if old == new: return @@ -235,8 +224,8 @@ def _from_iter(cls, label, itr): Returns ------- - cycler : Cycler - New 'base' `Cycler` + `Cycler` + New 'base' cycler. """ ret = cls(None) ret._left = list({label: v} for v in itr) @@ -264,12 +253,11 @@ def __iter__(self): def __add__(self, other): """ - Pair-wise combine two equal length cycles (zip) + Pair-wise combine two equal length cyclers (zip). Parameters ---------- other : Cycler - The second Cycler """ if len(self) != len(other): raise ValueError("Can only add equal length cycles, " @@ -278,13 +266,12 @@ def __add__(self, other): def __mul__(self, other): """ - Outer product of two cycles (`itertools.product`) or integer + Outer product of two cyclers (`itertools.product`) or integer multiplication. Parameters ---------- other : Cycler or int - The second Cycler or integer """ if isinstance(other, Cycler): return Cycler(self, other, product) @@ -307,12 +294,11 @@ def __len__(self): def __iadd__(self, other): """ - In-place pair-wise combine two equal length cycles (zip) + In-place pair-wise combine two equal length cyclers (zip). Parameters ---------- other : Cycler - The second Cycler """ if not isinstance(other, Cycler): raise TypeError("Cannot += with a non-Cycler object") @@ -326,12 +312,11 @@ def __iadd__(self, other): def __imul__(self, other): """ - In-place outer product of two cycles (`itertools.product`) + In-place outer product of two cyclers (`itertools.product`). Parameters ---------- other : Cycler - The second Cycler """ if not isinstance(other, Cycler): raise TypeError("Cannot *= with a non-Cycler object") @@ -344,14 +329,10 @@ def __imul__(self, other): return self def __eq__(self, other): - """ - Check equality - """ if len(self) != len(other): return False if self.keys ^ other.keys: return False - return all(a == b for a, b in zip(self, other)) def __ne__(self, other): @@ -385,7 +366,8 @@ def _repr_html_(self): return output def by_key(self): - """Values by key + """ + Values by key. This returns the transposed values of the cycler. Iterating over a `Cycler` yields dicts with a single value for each key, @@ -416,20 +398,18 @@ def by_key(self): _transpose = by_key def simplify(self): - """Simplify the Cycler - - Returned as a composition using only sums (no multiplications) + """ + Simplify the cycler into a sum (but no products) of cyclers. Returns ------- simple : Cycler - An equivalent cycler using only summation""" + """ # TODO: sort out if it is worth the effort to make sure this is # balanced. Currently it is is # (((a + b) + c) + d) vs # ((a + b) + (c + d)) # I would believe that there is some performance implications - trans = self.by_key() return reduce(add, (_cycler(k, v) for k, v in trans.items())) @@ -459,12 +439,10 @@ def cycler(*args, **kwargs): ---------- arg : Cycler Copy constructor for Cycler (does a shallow copy of iterables). - label : name The property key. In the 2-arg form of the function, the label can be any hashable object. In the keyword argument form of the function, it must be a valid python identifier. - itr : iterable Finite length iterable of the property values. Can be a single-property `Cycler` that would @@ -499,14 +477,12 @@ def cycler(*args, **kwargs): def _cycler(label, itr): """ - Create a new `Cycler` object from a property name and - iterable of values. + Create a new `Cycler` object from a property name and iterable of values. Parameters ---------- label : hashable The property key. - itr : iterable Finite length iterable of the property values. From 876853b9f5795aaa8dd61cf404235f84c012a14d Mon Sep 17 00:00:00 2001 From: Ayushman Singh Chauhan Date: Fri, 23 Apr 2021 23:52:08 +0530 Subject: [PATCH 30/98] Update index.rst DOC: Fix spelling --- doc/source/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index 518fb39..f262c17 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -11,9 +11,9 @@ ====== ==================================== -docs https://matplotlib.org/cycler -pypi https://pypi.python.org/pypi/Cycler -github https://github.com/matplotlib/cycler +Docs https://matplotlib.org/cycler +PyPI https://pypi.python.org/pypi/Cycler +GitHub https://github.com/matplotlib/cycler ====== ==================================== From 44e0d4e1b9222bae1db7b0bbac075fd26b10b997 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 4 Jun 2021 19:36:28 -0400 Subject: [PATCH 31/98] Remove run_tests.py It just wraps `pytest.main`, so that can just be used directly. --- MANIFEST.in | 3 +-- appveyor.yml | 6 +++--- run_tests.py | 11 ----------- 3 files changed, 4 insertions(+), 16 deletions(-) delete mode 100644 run_tests.py diff --git a/MANIFEST.in b/MANIFEST.in index 24bc16d..b357fbe 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ -include run_tests.py include test_cycler.py include LICENSE recursive-include conda-recipe * -recursive-include doc Makefile make.bat *.rst *.py \ No newline at end of file +recursive-include doc Makefile make.bat *.rst *.py diff --git a/appveyor.yml b/appveyor.yml index 12c23c7..6501efe 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,7 +20,7 @@ install: - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" # Install the build and runtime dependencies of the project. - - "pip install -v nose pytest pytest-cov coverage" + - "pip install -v pytest pytest-cov pytest-xdist" # Install the generated wheel package to test it - "python setup.py install" @@ -31,8 +31,8 @@ build: false test_script: - # Run unit tests with nose - - "python run_tests.py" + # Run unit tests with pytest + - "python -m pytest -raR -n auto" artifacts: # Archive the generated wheel package in the ci.appveyor.com build report. diff --git a/run_tests.py b/run_tests.py deleted file mode 100644 index 7efe075..0000000 --- a/run_tests.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -import sys -import pytest - -if __name__ == '__main__': - # 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)) From c07be8ee5be29852e7091c079fcc6ba0ac485d17 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 4 Jun 2021 19:50:04 -0400 Subject: [PATCH 32/98] Switch Travis CI to GitHub Actions. --- .github/workflows/tests.yml | 45 +++++++++++++++++++++++++++++++++++++ .travis.yml | 25 --------------------- 2 files changed, 45 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..3a2ae9b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,45 @@ +--- + +name: Tests + +on: + push: + branches-ignore: + - auto-backport-of-pr-[0-9]+ + - v[0-9]+.[0-9]+.[0-9x]+-doc + pull_request: + +jobs: + test: + name: "Python ${{ matrix.python-version }} ${{ matrix.name-suffix }}" + runs-on: ubuntu-18.04 + + strategy: + matrix: + python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9"] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade pytest pytest-cov pytest-xdist + + - name: Install cycler + run: | + python -m pip install --no-deps . + + - name: Run pytest + run: | + pytest -raR -n auto --cov --cov-report= + + - name: Upload code coverage + uses: codecov/codecov-action@v1 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9b41696..0000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: python - -matrix: - include: - - python: 2.7 - - python: 3.4 - - python: 3.5 - - python: 3.6 - - python: 3.7 - - python: "nightly" - env: PRE=--pre - allow_failures: - - python : "nightly" - -install: - - python -m pip install --upgrade pip - - python -m pip install -v . - - python -m pip install $PRE --upgrade pytest pytest-cov coverage - -script: - - coverage run run_tests.py - - coverage report -m - -after_success: - - bash <(curl -s https://codecov.io/bash) From 7f6963ba0f4a7de9ea6c0fb9e017b286383f0b1b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 4 Jun 2021 19:51:31 -0400 Subject: [PATCH 33/98] Add linting CI workflow. --- .github/workflows/reviewdog.yml | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/reviewdog.yml diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml new file mode 100644 index 0000000..14d8a71 --- /dev/null +++ b/.github/workflows/reviewdog.yml @@ -0,0 +1,35 @@ +--- +name: Linting +on: [pull_request] + +jobs: + flake8: + name: flake8 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Python 3 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + + - name: Install flake8 + run: pip3 install -r flake8 + + - name: Set up reviewdog + run: | + mkdir -p "$HOME/bin" + curl -sfL \ + https://github.com/reviewdog/reviewdog/raw/master/install.sh | \ + sh -s -- -b "$HOME/bin" + echo "$HOME/bin" >> $GITHUB_PATH + + - name: Run flake8 + env: + REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -o pipefail + flake8 --docstring-convention=all | \ + reviewdog -f=pep8 -name=flake8 \ + -tee -reporter=github-check -filter-mode nofilter From 396d23547ed55b0c0faac9cadc294b96e356046a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 4 Jun 2021 19:58:31 -0400 Subject: [PATCH 34/98] Fix flake8 issues. --- cycler.py | 4 +- doc/source/conf.py | 97 ++++++++++++++++++++++------------------------ test_cycler.py | 23 ++++++++++- 3 files changed, 70 insertions(+), 54 deletions(-) diff --git a/cycler.py b/cycler.py index f31fe85..903366c 100644 --- a/cycler.py +++ b/cycler.py @@ -242,8 +242,8 @@ def __getitem__(self, key): def __iter__(self): if self._right is None: - for l in self._left: - yield dict(l) + for left in self._left: + yield dict(left) else: for a, b in self._op(self._left, self._right): out = dict() diff --git a/doc/source/conf.py b/doc/source/conf.py index 2a4c3bd..b552145 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -13,13 +13,10 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys -import os - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ @@ -56,7 +53,7 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -76,13 +73,13 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -93,24 +90,24 @@ default_role = 'obj' # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output ---------------------------------------------- @@ -122,26 +119,26 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -151,48 +148,48 @@ # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'cyclerdoc' @@ -201,14 +198,14 @@ # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples @@ -221,23 +218,23 @@ # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -250,7 +247,7 @@ ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -265,19 +262,19 @@ ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False intersphinx_mapping = {'python': ('https://docs.python.org/3.4', None), 'matplotlb': ('https://matplotlib.org', None)} -################# numpydoc config #################### +# ################ numpydoc config #################### numpydoc_show_class_members = False diff --git a/test_cycler.py b/test_cycler.py index ded15c0..8b32dd0 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -178,8 +178,26 @@ def test_repr(): _repr_tester_helper('__repr__', c + c2, c_sum_rpr) _repr_tester_helper('__repr__', c * c2, c_prod_rpr) - sum_html = "
'3rd''c'
0'r'
1'g'
2'b'
" - prod_html = "
'3rd''c'
0'r'
1'r'
2'r'
0'g'
1'g'
2'g'
0'b'
1'b'
2'b'
" + sum_html = ( + "" + "" + "" + "" + "" + "
'3rd''c'
0'r'
1'g'
2'b'
") + prod_html = ( + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "
'3rd''c'
0'r'
1'r'
2'r'
0'g'
1'g'
2'g'
0'b'
1'b'
2'b'
") _repr_tester_helper('_repr_html_', c + c2, sum_html) _repr_tester_helper('_repr_html_', c * c2, prod_html) @@ -233,6 +251,7 @@ def test_copying(): 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']]) + assert c4 == cycler('bar', [['y', 'g', 'blue'], ['b', 'k']]) def test_keychange(): From 1987506ad41a9cb4ca296707ee94df33dfe4b752 Mon Sep 17 00:00:00 2001 From: Ali Muhammad Date: Sun, 27 Jun 2021 02:38:25 +0500 Subject: [PATCH 35/98] added format function --- cycler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cycler.py b/cycler.py index 903366c..9f948a4 100644 --- a/cycler.py +++ b/cycler.py @@ -185,11 +185,11 @@ def change_key(self, old, new): if old == new: return if new in self._keys: - raise ValueError("Can't replace %s with %s, %s is already a key" % - (old, new, new)) + raise ValueError("Can't replace {} with {}, {} is already a key" + .format(old, new, new)) if old not in self._keys: - raise KeyError("Can't replace %s with %s, %s is not a key" % - (old, new, old)) + raise KeyError("Can't replace {} with {}, {} is not a key" + .format(old, new, old)) self._keys.remove(old) self._keys.add(new) From 40c08a91d1e716e660dd552a16ae480677609f23 Mon Sep 17 00:00:00 2001 From: Ali Muhammad Date: Sun, 27 Jun 2021 02:42:41 +0500 Subject: [PATCH 36/98] improved some formatting --- test_cycler.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test_cycler.py b/test_cycler.py index 8b32dd0..97aaf0b 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -43,9 +43,9 @@ def test_add(): c1 = cycler(c='rgb') c2 = cycler(lw=range(3)) # addition - _cycler_helper(c1+c2, 3, ['c', 'lw'], [list('rgb'), range(3)]) - _cycler_helper(c2+c1, 3, ['c', 'lw'], [list('rgb'), range(3)]) - _cycles_equal(c2+c1, c1+c2) + _cycler_helper(c1 + c2, 3, ['c', 'lw'], [list('rgb'), range(3)]) + _cycler_helper(c2 + c1, 3, ['c', 'lw'], [list('rgb'), range(3)]) + _cycles_equal(c2 + c1, c1 + c2) def test_add_len_mismatch(): @@ -89,12 +89,12 @@ def test_inplace(): def test_constructor(): c1 = cycler(c='rgb') c2 = cycler(ec=c1) - _cycler_helper(c1+c2, 3, ['c', 'ec'], [['r', 'g', 'b']]*2) + _cycler_helper(c1 + c2, 3, ['c', 'ec'], [['r', 'g', 'b']] * 2) c3 = cycler(c=c1) - _cycler_helper(c3+c2, 3, ['c', 'ec'], [['r', 'g', 'b']]*2) + _cycler_helper(c3 + c2, 3, ['c', 'ec'], [['r', 'g', 'b']] * 2) # Using a non-string hashable c4 = cycler(1, range(3)) - _cycler_helper(c4+c1, 3, [1, 'c'], [range(3), ['r', 'g', 'b']]) + _cycler_helper(c4 + c1, 3, [1, 'c'], [range(3), ['r', 'g', 'b']]) # addition using cycler() _cycler_helper(cycler(c='rgb', lw=range(3)), @@ -118,7 +118,7 @@ def test_failures(): c3 = cycler(ec=c1) - pytest.raises(ValueError, cycler, c=c2+c3) + pytest.raises(ValueError, cycler, c=c2 + c3) def test_simplify(): @@ -130,12 +130,12 @@ def test_simplify(): def test_multiply(): c1 = cycler(c='rgb') - _cycler_helper(2*c1, 6, ['c'], ['rgb'*2]) + _cycler_helper(2 * c1, 6, ['c'], ['rgb' * 2]) c2 = cycler(ec=c1) c3 = c1 * c2 - _cycles_equal(2*c3, c3*2) + _cycles_equal(2 * c3, c3 * 2) def test_mul_fails(): @@ -208,7 +208,7 @@ def test_call(): c_cycle = c() assert isinstance(c_cycle, cycle) j = 0 - for a, b in zip(2*c, c_cycle): + for a, b in zip(2 * c, c_cycle): j += 1 assert a == b @@ -373,7 +373,7 @@ def test_contains(): assert 'a' not in b assert 'b' not in a - ab = a+b + ab = a + b assert 'a' in ab assert 'b' in ab From 01b247a663a67ce42fd95f21b8fb34eefd3b9a9a Mon Sep 17 00:00:00 2001 From: Ali Muhammad Date: Sun, 27 Jun 2021 02:57:33 +0500 Subject: [PATCH 37/98] added literal syntax instead dict --- cycler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cycler.py b/cycler.py index 9f948a4..9ee2f5f 100644 --- a/cycler.py +++ b/cycler.py @@ -246,7 +246,7 @@ def __iter__(self): yield dict(left) else: for a, b in self._op(self._left, self._right): - out = dict() + out = {} out.update(a) out.update(b) yield out From 92eca45c237c701336b5f092f387ae03c56652a2 Mon Sep 17 00:00:00 2001 From: Ali Muhammad Date: Mon, 28 Jun 2021 00:57:14 +0500 Subject: [PATCH 38/98] changed formatting with named placeholders --- cycler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cycler.py b/cycler.py index 9ee2f5f..2912dac 100644 --- a/cycler.py +++ b/cycler.py @@ -185,10 +185,10 @@ def change_key(self, old, new): if old == new: return if new in self._keys: - raise ValueError("Can't replace {} with {}, {} is already a key" + raise ValueError("Can't replace {old} with {new}, {new} is already a key" .format(old, new, new)) if old not in self._keys: - raise KeyError("Can't replace {} with {}, {} is not a key" + raise KeyError("Can't replace {old} with {new}, {old} is not a key" .format(old, new, old)) self._keys.remove(old) From 399d3e3cc2d25aa3b4631909e2969e2885874d48 Mon Sep 17 00:00:00 2001 From: Ali Muhammad Date: Mon, 28 Jun 2021 01:11:48 +0500 Subject: [PATCH 39/98] removed typo --- cycler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cycler.py b/cycler.py index 2912dac..7e47cae 100644 --- a/cycler.py +++ b/cycler.py @@ -186,10 +186,10 @@ def change_key(self, old, new): return if new in self._keys: raise ValueError("Can't replace {old} with {new}, {new} is already a key" - .format(old, new, new)) + .format(old=old, new=new)) if old not in self._keys: raise KeyError("Can't replace {old} with {new}, {old} is not a key" - .format(old, new, old)) + .format(old=old, new=new)) self._keys.remove(old) self._keys.add(new) From af041d7cd47d0644becfdbd15f3ae9665eec93fb Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 29 Jun 2021 04:32:30 -0400 Subject: [PATCH 40/98] Fix setup of linting GitHub Action. --- .github/workflows/reviewdog.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index 14d8a71..1e4d8b2 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -15,7 +15,7 @@ jobs: python-version: 3.8 - name: Install flake8 - run: pip3 install -r flake8 + run: pip3 install flake8 - name: Set up reviewdog run: | @@ -30,6 +30,6 @@ jobs: REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -o pipefail - flake8 --docstring-convention=all | \ + flake8 | \ reviewdog -f=pep8 -name=flake8 \ -tee -reporter=github-check -filter-mode nofilter From 0a53b361ad46809c9fcb226ac2e2036389ca7938 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 29 Jun 2021 04:34:29 -0400 Subject: [PATCH 41/98] Fix flake8 errors in tests. --- test_cycler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_cycler.py b/test_cycler.py index 8b32dd0..e78c0c8 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -11,8 +11,8 @@ if sys.version_info < (3,): from itertools import izip as zip - range = xrange - str = unicode + range = xrange # noqa + str = unicode # noqa def _cycler_helper(c, length, keys, values): From e98960a402e4cdb2a4725c22ba1d6abfb95bae76 Mon Sep 17 00:00:00 2001 From: Ali Muhammad Date: Tue, 29 Jun 2021 13:49:44 +0500 Subject: [PATCH 42/98] pylint problem solved --- cycler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cycler.py b/cycler.py index 7e47cae..0c89e49 100644 --- a/cycler.py +++ b/cycler.py @@ -185,8 +185,10 @@ def change_key(self, old, new): if old == new: return if new in self._keys: - raise ValueError("Can't replace {old} with {new}, {new} is already a key" - .format(old=old, new=new)) + raise ValueError( + "Can't replace {old} with {new}, {new} is already a key" + .format(old=old, new=new) + ) if old not in self._keys: raise KeyError("Can't replace {old} with {new}, {old} is not a key" .format(old=old, new=new)) From a6edfc2ebdde7e8a59842d6f0204e671a387ebec Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 17 Aug 2021 16:39:15 +0300 Subject: [PATCH 43/98] Add support for Python 3.7-3.9 --- appveyor.yml | 8 ++++++++ setup.py | 3 +++ 2 files changed, 11 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 6501efe..7764555 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,8 @@ # AppVeyor.com is a Continuous Integration service to build and run tests under # Windows +image: Visual Studio 2019 + environment: matrix: - PYTHON: "C:\\Python27" @@ -11,6 +13,12 @@ environment: - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python36-x64" + - PYTHON: "C:\\Python37" + - PYTHON: "C:\\Python37-x64" + - PYTHON: "C:\\Python38" + - PYTHON: "C:\\Python38-x64" + - PYTHON: "C:\\Python39" + - PYTHON: "C:\\Python39-x64" install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" diff --git a/setup.py b/setup.py index 8b50257..f3193b8 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,9 @@ 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], keywords='cycle kwargs', ) From bd2442b65f575859928e3fe6639f2b662c5e7405 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 17 Aug 2021 16:48:34 +0300 Subject: [PATCH 44/98] Drop support for EOL Python 2.7 and 3.4-3.5 --- .github/workflows/tests.yml | 4 ++-- appveyor.yml | 6 ------ setup.py | 7 ++----- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3a2ae9b..f073e1b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,11 +12,11 @@ on: jobs: test: name: "Python ${{ matrix.python-version }} ${{ matrix.name-suffix }}" - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9"] + python-version: ["3.6", "3.7", "3.8", "3.9"] steps: - uses: actions/checkout@v2 diff --git a/appveyor.yml b/appveyor.yml index 7764555..a981d68 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,12 +5,6 @@ image: Visual Studio 2019 environment: matrix: - - PYTHON: "C:\\Python27" - - PYTHON: "C:\\Python27-x64" - - PYTHON: "C:\\Python34" - - PYTHON: "C:\\Python34-x64" - - PYTHON: "C:\\Python35" - - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37" diff --git a/setup.py b/setup.py index f3193b8..4baea80 100644 --- a/setup.py +++ b/setup.py @@ -9,18 +9,15 @@ url='https://github.com/matplotlib/cycler', platforms='Cross platform (Linux, Mac OSX, Windows)', license="BSD", - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + python_requires='>=3.6', classifiers=['License :: OSI Approved :: BSD License', 'Development Status :: 4 - Beta', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3 :: Only', ], keywords='cycle kwargs', ) From 7773b0d0ebdab0b21e7a4679dbceaa58d91acd36 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 17 Aug 2021 16:49:12 +0300 Subject: [PATCH 45/98] Upgrade Python syntax with pyupgrade --py36-plus --- cycler.py | 14 ++++++-------- doc/source/conf.py | 1 - test_cycler.py | 2 -- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/cycler.py b/cycler.py index 0c89e49..49c98da 100644 --- a/cycler.py +++ b/cycler.py @@ -40,8 +40,6 @@ {'color': 'b', 'linestyle': '-.'} """ -from __future__ import (absolute_import, division, print_function, - unicode_literals) import copy from functools import reduce @@ -107,7 +105,7 @@ def concat(left, right): return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys)) -class Cycler(object): +class Cycler: """ Composable cycles. @@ -231,7 +229,7 @@ def _from_iter(cls, label, itr): """ ret = cls(None) ret._left = list({label: v} for v in itr) - ret._keys = set([label]) + ret._keys = {label} return ret def __getitem__(self, key): @@ -263,7 +261,7 @@ def __add__(self, other): """ if len(self) != len(other): raise ValueError("Can only add equal length cycles, " - "not {0} and {1}".format(len(self), len(other))) + "not {} and {}".format(len(self), len(other))) return Cycler(self, other, zip) def __mul__(self, other): @@ -347,7 +345,7 @@ def __repr__(self): if self._right is None: lab = self.keys.pop() itr = list(v[lab] for v in self) - return "cycler({lab!r}, {itr!r})".format(lab=lab, itr=itr) + return f"cycler({lab!r}, {itr!r})" else: op = op_map.get(self._op, '?') msg = "({left!r} {op} {right!r})" @@ -358,11 +356,11 @@ def _repr_html_(self): output = "" sorted_keys = sorted(self.keys, key=repr) for key in sorted_keys: - output += "".format(key=key) + output += f"" for d in iter(self): output += "" for k in sorted_keys: - output += "".format(val=d[k]) + output += f"" output += "" output += "
{key!r}{key!r}
{val!r}{d[k]!r}
" return output diff --git a/doc/source/conf.py b/doc/source/conf.py index b552145..dd5defd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # cycler documentation build configuration file, created by # sphinx-quickstart on Wed Jul 1 13:32:53 2015. diff --git a/test_cycler.py b/test_cycler.py index c511db4..164d673 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -1,5 +1,3 @@ -from __future__ import (absolute_import, division, print_function) - from collections import defaultdict from operator import add, iadd, mul, imul from itertools import product, cycle, chain From 2a8831489b8ea05614459afb3135db3248890268 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 17 Aug 2021 16:51:41 +0300 Subject: [PATCH 46/98] Drop support for EOL Python 2.7 and 3.4 --- cycler.py | 4 ---- doc/source/conf.py | 2 +- test_cycler.py | 6 ------ 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/cycler.py b/cycler.py index 49c98da..9e5724d 100644 --- a/cycler.py +++ b/cycler.py @@ -45,10 +45,6 @@ from functools import reduce from itertools import product, cycle from operator import mul, add -import sys - -if sys.version_info < (3,): - from itertools import izip as zip __version__ = '0.10.0' diff --git a/doc/source/conf.py b/doc/source/conf.py index dd5defd..0d60a5b 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -272,7 +272,7 @@ # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False -intersphinx_mapping = {'python': ('https://docs.python.org/3.4', None), +intersphinx_mapping = {'python': ('https://docs.python.org/3', None), 'matplotlb': ('https://matplotlib.org', None)} # ################ numpydoc config #################### diff --git a/test_cycler.py b/test_cycler.py index 164d673..67f708f 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -1,17 +1,11 @@ from collections import defaultdict from operator import add, iadd, mul, imul from itertools import product, cycle, chain -import sys import pytest from cycler import cycler, Cycler, concat -if sys.version_info < (3,): - from itertools import izip as zip - range = xrange # noqa - str = unicode # noqa - def _cycler_helper(c, length, keys, values): assert len(c) == length From f09469c20b9cb782db64edc23f80c4ebe5999188 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 17 Aug 2021 16:57:16 +0300 Subject: [PATCH 47/98] Add support for Python 3.10 --- .github/workflows/tests.yml | 2 +- setup.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f073e1b..46f927f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"] steps: - uses: actions/checkout@v2 diff --git a/setup.py b/setup.py index 4baea80..c639e92 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ py_modules=['cycler'], description='Composable style cycles', url='https://github.com/matplotlib/cycler', - platforms='Cross platform (Linux, Mac OSX, Windows)', + platforms='Cross platform (Linux, macOS, Windows)', license="BSD", python_requires='>=3.6', classifiers=['License :: OSI Approved :: BSD License', @@ -17,6 +17,7 @@ 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3 :: Only', ], keywords='cycle kwargs', From 2c647dab4e52917e354685a05ea2ea8732d5570d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 18 Aug 2021 00:00:11 +0300 Subject: [PATCH 48/98] f-stringify --- cycler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cycler.py b/cycler.py index 9e5724d..f86b68d 100644 --- a/cycler.py +++ b/cycler.py @@ -257,7 +257,7 @@ def __add__(self, other): """ if len(self) != len(other): raise ValueError("Can only add equal length cycles, " - "not {} and {}".format(len(self), len(other))) + f"not {len(self)} and {len(other)}") return Cycler(self, other, zip) def __mul__(self, other): From 1b2a54b34859d2a0736de90400401111aa48b915 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 25 Oct 2021 17:33:44 -0400 Subject: [PATCH 49/98] Remove conda recipe This is a downstream packaging concern, and conda-forge or anaconda can take care of it. --- conda-recipe/bld.bat | 8 ------ conda-recipe/build.sh | 9 ------- conda-recipe/meta.yaml | 61 ------------------------------------------ 3 files changed, 78 deletions(-) delete mode 100644 conda-recipe/bld.bat delete mode 100644 conda-recipe/build.sh delete mode 100644 conda-recipe/meta.yaml diff --git a/conda-recipe/bld.bat b/conda-recipe/bld.bat deleted file mode 100644 index 2d84cc9..0000000 --- a/conda-recipe/bld.bat +++ /dev/null @@ -1,8 +0,0 @@ -"%PYTHON%" setup.py install -if errorlevel 1 exit 1 - -:: Add more build steps here, if they are necessary. - -:: See -:: https://conda.io/docs/user-guide/tasks/build-packages/index.html -:: for a list of environment variables that are set during the build process. diff --git a/conda-recipe/build.sh b/conda-recipe/build.sh deleted file mode 100644 index 4c6226d..0000000 --- a/conda-recipe/build.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -$PYTHON setup.py install - -# Add more build steps here, if they are necessary. - -# See -# https://conda.io/docs/user-guide/tasks/build-packages/index.html -# for a list of environment variables that are set during the build process. diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml deleted file mode 100644 index cab179e..0000000 --- a/conda-recipe/meta.yaml +++ /dev/null @@ -1,61 +0,0 @@ -package: - name: cycler - version: {{ environ['GIT_DESCRIBE_TAG'] }}.post{{ environ['GIT_DESCRIBE_NUMBER'] }} - - -source: - git_url: ../ - -# patches: - # List any patch files here - # - fix.patch - -build: - string: {{ environ.get('GIT_BUILD_STR', '') }}_py{{ py }} - # preserve_egg_dir: True - # entry_points: - # Put any entry points (scripts to be generated automatically) here. The - # syntax is module:function. For example - # - # - cycler = cycler:main - # - # Would create an entry point called cycler that calls cycler.main() - - - # If this is a new build for the same version, increment the build - # number. If you do not include this key, it defaults to 0. - # number: 1 - -requirements: - build: - - python - - setuptools - - run: - - python - -test: - # Python imports - imports: - - cycler - - # commands: - # You can put test commands to be run here. Use this to test that the - # entry points work. - - - # You can also put a file called run_test.py in the recipe that will be run - # at test time. - - # requires: - # Put any additional test requirements here. For example - # - nose - -about: - home: https://github.com/matplotlib/cycler - license: BSD - summary: 'Composable style cycles' - -# See -# https://conda.io/docs/building/build.html for -# more information about meta.yaml From ad42f466f34f6d1613b31cdc009ecc51d7c085c0 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 28 Oct 2021 23:30:12 -0400 Subject: [PATCH 50/98] Clean up sdist contents --- MANIFEST.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index b357fbe..e6bfad6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include test_cycler.py include LICENSE -recursive-include conda-recipe * recursive-include doc Makefile make.bat *.rst *.py +exclude appveyor.yml +recursive-exclude .github * From 0f33326ccc5c4b001ff097c2505c6b46ea0125c9 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 28 Oct 2021 23:05:01 -0400 Subject: [PATCH 51/98] REL: v0.11 This is the first Cycler feature release in some years. New features include: * Added `Cycler.by_key`, which produces values by key (#26) * Added `Cycler.__contains__`, which adds support for `in` checks (#34) * Wheels now includes the LICENSE file (#48) * The sdist now includes the LICENSE (#58) and tests (#32) * Cycler no longer supports Python 2. Supported versions of Python are 3.6 and above. --- doc/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 0d60a5b..bb1fc6b 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -66,9 +66,9 @@ # built documents. # # The short X.Y version. -version = '0.10.0' +version = '0.11.0' # The full version, including alpha/beta/rc tags. -release = '0.10.0' +release = '0.11.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index c639e92..3c5f39d 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='cycler', - version='0.10.0', + version='0.11.0', author='Thomas A Caswell', author_email='matplotlib-users@python.org', py_modules=['cycler'], From bce717b4bec76858c86cee7454aaa6fa8540b916 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 28 Oct 2021 23:38:38 -0400 Subject: [PATCH 52/98] BLD: Bump version for next release --- doc/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index bb1fc6b..d58c79b 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -66,9 +66,9 @@ # built documents. # # The short X.Y version. -version = '0.11.0' +version = '0.12.0' # The full version, including alpha/beta/rc tags. -release = '0.11.0' +release = '0.12.0.dev0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 3c5f39d..2163dd4 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='cycler', - version='0.11.0', + version='0.12.0.dev0', author='Thomas A Caswell', author_email='matplotlib-users@python.org', py_modules=['cycler'], From 478ce549039527001b9d0418ddda5091c75a6095 Mon Sep 17 00:00:00 2001 From: Ewout ter Hoeven Date: Sat, 30 Oct 2021 12:07:09 +0200 Subject: [PATCH 53/98] CI: Update to codecov-action@v2, Python 3.10-dev to 3.10 On February 1, 2022, the codecov-action v1 will be end of life and not longer function. https://github.com/codecov/codecov-action/tree/v2.1.0#%EF%B8%8F--deprecration-of-v1. This commit updates the action to v2. Also, the Python 3.10-dev tag is replaced with 3.10 since it's in stable release now. --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 46f927f..10cc2fa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 @@ -42,4 +42,4 @@ jobs: pytest -raR -n auto --cov --cov-report= - name: Upload code coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v2 From 0df24bf93401fe06e0694d38492129807c8f11aa Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 1 Mar 2022 00:04:27 +0100 Subject: [PATCH 54/98] Simple cleanups. `__ne__` is specced to default to `not __eq__`, so we may just as well use that. Inline "test helpers" which probably made sense back when using nose, but not so much nowadays anymore. --- cycler.py | 3 --- test_cycler.py | 45 ++++++++++++++------------------------------- 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/cycler.py b/cycler.py index f86b68d..222a617 100644 --- a/cycler.py +++ b/cycler.py @@ -331,9 +331,6 @@ def __eq__(self, other): return False return all(a == b for a, b in zip(self, other)) - def __ne__(self, other): - return not (self == other) - __hash__ = None def __repr__(self): diff --git a/test_cycler.py b/test_cycler.py index 67f708f..a9130ff 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -153,31 +153,24 @@ def test_fail_getime(): pytest.raises(ValueError, Cycler.__getitem__, c1, [0, 1]) -def _repr_tester_helper(rpr_func, cyc, target_repr): - test_repr = getattr(cyc, rpr_func)() - - assert str(test_repr) == str(target_repr) - - def test_repr(): c = cycler(c='rgb') # Using an identifier that would be not valid as a kwarg c2 = cycler('3rd', range(3)) - c_sum_rpr = "(cycler('c', ['r', 'g', 'b']) + cycler('3rd', [0, 1, 2]))" - c_prod_rpr = "(cycler('c', ['r', 'g', 'b']) * cycler('3rd', [0, 1, 2]))" - - _repr_tester_helper('__repr__', c + c2, c_sum_rpr) - _repr_tester_helper('__repr__', c * c2, c_prod_rpr) + assert repr(c + c2) == ( + "(cycler('c', ['r', 'g', 'b']) + cycler('3rd', [0, 1, 2]))") + assert repr(c * c2) == ( + "(cycler('c', ['r', 'g', 'b']) * cycler('3rd', [0, 1, 2]))") - sum_html = ( + assert (c + c2)._repr_html_() == ( "" "" "" "" "" "
'3rd''c'
0'r'
1'g'
2'b'
") - prod_html = ( + assert (c * c2)._repr_html_() == ( "" "" "" @@ -191,9 +184,6 @@ def test_repr(): "" "
'3rd''c'
0'r'
2'b'
") - _repr_tester_helper('_repr_html_', c + c2, sum_html) - _repr_tester_helper('_repr_html_', c * c2, prod_html) - def test_call(): c = cycler(c='rgb') @@ -276,27 +266,20 @@ def test_keychange(): pytest.raises(KeyError, Cycler.change_key, c, 'c', 'foobar') -def _eq_test_helper(a, b, res): - if res: - assert a == b - else: - assert a != b - - def test_eq(): a = cycler(c='rgb') b = cycler(c='rgb') - _eq_test_helper(a, b, True) - _eq_test_helper(a, b[::-1], False) + assert a == b + assert a != b[::-1] c = cycler(lw=range(3)) - _eq_test_helper(a+c, c+a, True) - _eq_test_helper(a+c, c+b, True) - _eq_test_helper(a*c, c*a, False) - _eq_test_helper(a, c, False) + assert a + c == c + a + assert a + c == c + b + assert a * c != c * a + assert a != c d = cycler(c='ymk') - _eq_test_helper(b, d, False) + assert b != d e = cycler(c='orange') - _eq_test_helper(b, e, False) + assert b != e def test_cycler_exceptions(): From d8e72d919310bf00e3c718ed401ccbd5e3694d57 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Tue, 1 Mar 2022 10:11:30 +0100 Subject: [PATCH 55/98] Update version. Closes #80 --- cycler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cycler.py b/cycler.py index 222a617..65e95ac 100644 --- a/cycler.py +++ b/cycler.py @@ -46,7 +46,7 @@ from itertools import product, cycle from operator import mul, add -__version__ = '0.10.0' +__version__ = '0.12.0.dev0' def _process_keys(left, right): From e6e12d909f8dfdee27913f36360086f795cf4bdf Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Tue, 1 Mar 2022 10:11:34 +0100 Subject: [PATCH 56/98] Remove 3.6 support and update Python setup version --- .github/workflows/reviewdog.yml | 4 ++-- .github/workflows/tests.yml | 4 ++-- setup.py | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index 1e4d8b2..064b947 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -10,9 +10,9 @@ jobs: - uses: actions/checkout@v2 - name: Set up Python 3 - uses: actions/setup-python@v1 + uses: actions/setup-python@v3 with: - python-version: 3.8 + python-version: 3.9 - name: Install flake8 run: pip3 install flake8 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 10cc2fa..08ffb7e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} diff --git a/setup.py b/setup.py index 2163dd4..5e0e6b4 100644 --- a/setup.py +++ b/setup.py @@ -9,11 +9,10 @@ url='https://github.com/matplotlib/cycler', platforms='Cross platform (Linux, macOS, Windows)', license="BSD", - python_requires='>=3.6', + python_requires='>=3.7', classifiers=['License :: OSI Approved :: BSD License', 'Development Status :: 4 - Beta', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', From ea73f0038b057601620e0ce4988db5890ca6819b Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 31 Oct 2022 18:21:43 -0400 Subject: [PATCH 57/98] GOV: add security reporting policy --- SECURITY.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b134e78 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + + +## Reporting a Vulnerability + + +To report a security vulnerability, please use the [Tidelift security +contact](https://tidelift.com/security). Tidelift will coordinate the fix and +disclosure. + +If you have found a security vulnerability, in order to keep it confidential, +please do not report an issue on GitHub. + +We do not award bounties for security vulnerabilities. From 1cb67ea264f952e5413c436ff2bfdc182294c9e1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 16 Nov 2022 21:34:46 +0200 Subject: [PATCH 58/98] Add support for Python 3.11 --- .github/workflows/reviewdog.yml | 4 ++-- .github/workflows/tests.yml | 8 ++++---- setup.py | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index 064b947..1137693 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -7,10 +7,10 @@ jobs: name: flake8 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: 3.9 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 08ffb7e..695f814 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,15 +16,15 @@ jobs: strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} @@ -42,4 +42,4 @@ jobs: pytest -raR -n auto --cov --cov-report= - name: Upload code coverage - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 diff --git a/setup.py b/setup.py index 5e0e6b4..d4dc139 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3 :: Only', ], keywords='cycle kwargs', From 2259efea5150bd51433c468e1022c1662c214f42 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 7 Dec 2022 23:31:51 -0600 Subject: [PATCH 59/98] Mypy Compatible tests for cycler - Convert to package instead of single file module - add py.typed (including setup.py changes) - add a CI workflow to run mypy - Add the actual type hints inline Type hints are generic over both the key type (most commonly strings) and the value type. I initially tried to be extra cute and encompass the type expansion that can happen when cyclers of different types are added/multiplied, but that proved to be more of a hassle to deal with than I was willing to deal with initially. As such, the example from the `concat` docstring will actually fail to properly type check with implicit types (as Cycler[str, int] cannot be concat'd with Cycler[str, str] and properly typecheck) This may be solvable (resulting in a Cycler[str, int|str]) but that is not yet implemented. It may be resolved in downstream code somewhat crudely by typing both operands explicitly as Cycler[str, int|str] from the outset. Outside of that corner case, should be entirely correct, I believe. --- .github/workflows/mypy.yml | 36 ++++++++++++ cycler.py => cycler/__init__.py | 100 +++++++++++++++++++------------- cycler/py.typed | 0 setup.py | 5 +- 4 files changed, 99 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/mypy.yml rename cycler.py => cycler/__init__.py (81%) create mode 100644 cycler/py.typed diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 0000000..55d87a4 --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,36 @@ +--- + +name: Mypy + +on: + push: + branches-ignore: + - auto-backport-of-pr-[0-9]+ + - v[0-9]+.[0-9]+.[0-9x]+-doc + pull_request: + +jobs: + test: + name: "Mypy" + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade mypy + + - name: Install cycler + run: | + python -m pip install --no-deps . + + - name: Run mypy + run: | + mypy cycler test_cycler.py diff --git a/cycler.py b/cycler/__init__.py similarity index 81% rename from cycler.py rename to cycler/__init__.py index 65e95ac..3b7c9fd 100644 --- a/cycler.py +++ b/cycler/__init__.py @@ -41,15 +41,21 @@ """ +from __future__ import annotations + +from collections.abc import Hashable, Iterable, Callable import copy from functools import reduce from itertools import product, cycle from operator import mul, add +from typing import TypeVar, Generic, Generator, Any, overload __version__ = '0.12.0.dev0' +K = TypeVar("K", bound=Hashable) +V = TypeVar("V") -def _process_keys(left, right): +def _process_keys(left: Cycler[K, V]|Iterable[dict[K,V]]|None, right: Cycler[K, V]|Iterable[dict[K,V]]|None) -> set[K]: """ Helper function to compose cycler keys. @@ -63,16 +69,16 @@ def _process_keys(left, right): keys : set The keys in the composition of the two cyclers. """ - l_peek = next(iter(left)) if left is not None else {} - r_peek = next(iter(right)) if right is not None else {} - l_key = set(l_peek.keys()) - r_key = set(r_peek.keys()) + l_peek: dict[K, V] = next(iter(left)) if left is not None else {} + r_peek: dict[K, V] = next(iter(right)) if right is not None else {} + l_key: set[K] = set(l_peek.keys()) + r_key: set[K] = set(r_peek.keys()) if l_key & r_key: raise ValueError("Can not compose overlapping cycles") return l_key | r_key -def concat(left, right): +def concat(left: Cycler[K, V], right: Cycler[K, V]) -> Cycler[K, V]: r""" Concatenate `Cycler`\s, as if chained using `itertools.chain`. @@ -100,8 +106,7 @@ def concat(left, right): _r = right.by_key() return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys)) - -class Cycler: +class Cycler(Generic[K, V]): """ Composable cycles. @@ -132,14 +137,14 @@ class Cycler: def __call__(self): return cycle(self) - def __init__(self, left, right=None, op=None): + def __init__(self, left: Cycler[K, V] | Iterable[dict[K,V]] | None, right: Cycler[K, V] | Iterable[dict[K,V]] | None=None, op: Any=None): """ Semi-private init. Do not use this directly, use `cycler` function instead. """ if isinstance(left, Cycler): - self._left = Cycler(left._left, left._right, left._op) + self._left: Cycler[K, V] | list[dict[K,V]] | None = Cycler(left._left, left._right, left._op) elif left is not None: # Need to copy the dictionary or else that will be a residual # mutable that could lead to strange errors @@ -148,7 +153,7 @@ def __init__(self, left, right=None, op=None): self._left = None if isinstance(right, Cycler): - self._right = Cycler(right._left, right._right, right._op) + self._right: Cycler[K, V] | list[dict[K,V]] | None = Cycler(right._left, right._right, right._op) elif right is not None: # Need to copy the dictionary or else that will be a residual # mutable that could lead to strange errors @@ -156,18 +161,18 @@ def __init__(self, left, right=None, op=None): else: self._right = None - self._keys = _process_keys(self._left, self._right) - self._op = op + self._keys: set[K] = _process_keys(self._left, self._right) + self._op: Any = op def __contains__(self, k): return k in self._keys @property - def keys(self): + def keys(self) -> set[K]: """The keys this Cycler knows about.""" return set(self._keys) - def change_key(self, old, new): + def change_key(self, old: K, new: K) -> None: """ Change a key in this cycler to a new name. Modification is performed in-place. @@ -190,11 +195,12 @@ def change_key(self, old, new): self._keys.remove(old) self._keys.add(new) - if self._right is not None and old in self._right.keys: + if self._right is not None and isinstance(self._right, Cycler) and old in self._right.keys: self._right.change_key(old, new) # self._left should always be non-None # if self._keys is non-empty. + elif self._left is None: pass elif isinstance(self._left, Cycler): self._left.change_key(old, new) else: @@ -204,7 +210,7 @@ def change_key(self, old, new): self._left = [{new: entry[old]} for entry in self._left] @classmethod - def _from_iter(cls, label, itr): + def _from_iter(cls, label: K, itr: Iterable[V]) -> Cycler[K, V]: """ Class method to create 'base' Cycler objects that do not have a 'right' or 'op' and for which @@ -212,7 +218,7 @@ def _from_iter(cls, label, itr): Parameters ---------- - label : str + label : hashable The property key. itr : iterable @@ -223,12 +229,12 @@ def _from_iter(cls, label, itr): `Cycler` New 'base' cycler. """ - ret = cls(None) + ret: Cycler[K, V] = cls(None) ret._left = list({label: v} for v in itr) ret._keys = {label} return ret - def __getitem__(self, key): + def __getitem__(self, key: slice) -> Cycler[K, V]: # TODO : maybe add numpy style fancy slicing if isinstance(key, slice): trans = self.by_key() @@ -236,18 +242,21 @@ def __getitem__(self, key): else: raise ValueError("Can only use slices with Cycler.__getitem__") - def __iter__(self): - if self._right is None: - for left in self._left: - yield dict(left) + def __iter__(self) -> Generator[dict[K, V], None, None]: + if self._right is None or self._left is None: + if self._left is not None: + for left in self._left: + yield dict(left) else: + if self._op is None: + raise TypeError("Operation cannot be None when both left and right are defined") for a, b in self._op(self._left, self._right): out = {} out.update(a) out.update(b) yield out - def __add__(self, other): + def __add__(self, other: Cycler[K, V]) -> Cycler[K, V]: """ Pair-wise combine two equal length cyclers (zip). @@ -260,7 +269,7 @@ def __add__(self, other): f"not {len(self)} and {len(other)}") return Cycler(self, other, zip) - def __mul__(self, other): + def __mul__(self, other: Cycler[K, V] | int) -> Cycler[K, V]: """ Outer product of two cyclers (`itertools.product`) or integer multiplication. @@ -277,18 +286,22 @@ def __mul__(self, other): else: return NotImplemented - def __rmul__(self, other): + def __rmul__(self, other: Cycler[K, V]) -> Cycler[K, V]: return self * other - def __len__(self): + def __len__(self) -> int: op_dict = {zip: min, product: mul} + if self._left is None: + if self._left is None: + return 0 + return 0 if self._right is None: return len(self._left) l_len = len(self._left) r_len = len(self._right) - return op_dict[self._op](l_len, r_len) + return op_dict[self._op](l_len, r_len) # type: ignore - def __iadd__(self, other): + def __iadd__(self, other: Cycler[K, V]) -> Cycler[K, V]: """ In-place pair-wise combine two equal length cyclers (zip). @@ -306,7 +319,7 @@ def __iadd__(self, other): self._right = Cycler(other._left, other._right, other._op) return self - def __imul__(self, other): + def __imul__(self, other: Cycler[K,V]|int) -> Cycler[K, V]: """ In-place outer product of two cyclers (`itertools.product`). @@ -324,16 +337,18 @@ def __imul__(self, other): self._right = Cycler(other._left, other._right, other._op) return self - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, Cycler): + return False if len(self) != len(other): return False if self.keys ^ other.keys: return False return all(a == b for a, b in zip(self, other)) - __hash__ = None + __hash__ = None # type: ignore - def __repr__(self): + def __repr__(self) -> str: op_map = {zip: '+', product: '*'} if self._right is None: lab = self.keys.pop() @@ -344,7 +359,7 @@ def __repr__(self): msg = "({left!r} {op} {right!r})" return msg.format(left=self._left, op=op, right=self._right) - def _repr_html_(self): + def _repr_html_(self) -> str: # an table showing the value of each key through a full cycle output = "" sorted_keys = sorted(self.keys, key=repr) @@ -358,7 +373,7 @@ def _repr_html_(self): output += "
" return output - def by_key(self): + def by_key(self) -> dict[K, list[V]]: """ Values by key. @@ -380,7 +395,7 @@ def by_key(self): # and if we care. keys = self.keys - out = {k: list() for k in keys} + out: dict[K, list[V]] = {k: list() for k in keys} for d in self: for k in keys: @@ -390,7 +405,7 @@ def by_key(self): # for back compatibility _transpose = by_key - def simplify(self): + def simplify(self) -> Cycler[K, V]: """ Simplify the cycler into a sum (but no products) of cyclers. @@ -408,7 +423,12 @@ def simplify(self): concat = concat - +@overload +def cycler(args: Cycler[K, V]) -> Cycler[K, V]: ... +@overload +def cycler(**kwargs: Iterable[V]) -> Cycler[str, V]: ... +@overload +def cycler(label: K, itr: Iterable[V]) -> Cycler[K, V]: ... def cycler(*args, **kwargs): """ Create a new `Cycler` object from a single positional argument, @@ -468,7 +488,7 @@ def cycler(*args, **kwargs): raise TypeError("Must have at least a positional OR keyword arguments") -def _cycler(label, itr): +def _cycler(label: K, itr: Iterable[V]) -> Cycler[K, V]: """ Create a new `Cycler` object from a property name and iterable of values. diff --git a/cycler/py.typed b/cycler/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index d4dc139..bc5880d 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,11 @@ -from setuptools import setup +from setuptools import setup, find_packages setup(name='cycler', version='0.12.0.dev0', author='Thomas A Caswell', author_email='matplotlib-users@python.org', - py_modules=['cycler'], + packages=find_packages(), + package_data = {"cycler": ["py.typed"]}, description='Composable style cycles', url='https://github.com/matplotlib/cycler', platforms='Cross platform (Linux, macOS, Windows)', From b7c1e16e9cb778414113d0c3587e9fe7e39322a1 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 7 Dec 2022 23:59:00 -0600 Subject: [PATCH 60/98] blacken Only lines (almost) it touches outside of what is added for typing are error messages --- cycler/__init__.py | 107 +++++++++++++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 32 deletions(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index 3b7c9fd..3efb050 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -50,12 +50,16 @@ from operator import mul, add from typing import TypeVar, Generic, Generator, Any, overload -__version__ = '0.12.0.dev0' +__version__ = "0.12.0.dev0" K = TypeVar("K", bound=Hashable) V = TypeVar("V") -def _process_keys(left: Cycler[K, V]|Iterable[dict[K,V]]|None, right: Cycler[K, V]|Iterable[dict[K,V]]|None) -> set[K]: + +def _process_keys( + left: Cycler[K, V] | Iterable[dict[K, V]] | None, + right: Cycler[K, V] | Iterable[dict[K, V]] | None, +) -> set[K]: """ Helper function to compose cycler keys. @@ -97,15 +101,18 @@ def concat(left: Cycler[K, V], right: Cycler[K, V]) -> Cycler[K, V]: The concatenated cycler. """ if left.keys != right.keys: - raise ValueError("Keys do not match:\n" - "\tIntersection: {both!r}\n" - "\tDisjoint: {just_one!r}".format( - both=left.keys & right.keys, - just_one=left.keys ^ right.keys)) + raise ValueError( + "Keys do not match:\n" + "\tIntersection: {both!r}\n" + "\tDisjoint: {just_one!r}".format( + both=left.keys & right.keys, just_one=left.keys ^ right.keys + ) + ) _l = left.by_key() _r = right.by_key() return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys)) + class Cycler(Generic[K, V]): """ Composable cycles. @@ -137,14 +144,21 @@ class Cycler(Generic[K, V]): def __call__(self): return cycle(self) - def __init__(self, left: Cycler[K, V] | Iterable[dict[K,V]] | None, right: Cycler[K, V] | Iterable[dict[K,V]] | None=None, op: Any=None): + def __init__( + self, + left: Cycler[K, V] | Iterable[dict[K, V]] | None, + right: Cycler[K, V] | Iterable[dict[K, V]] | None = None, + op: Any = None, + ): """ Semi-private init. Do not use this directly, use `cycler` function instead. """ if isinstance(left, Cycler): - self._left: Cycler[K, V] | list[dict[K,V]] | None = Cycler(left._left, left._right, left._op) + self._left: Cycler[K, V] | list[dict[K, V]] | None = Cycler( + left._left, left._right, left._op + ) elif left is not None: # Need to copy the dictionary or else that will be a residual # mutable that could lead to strange errors @@ -153,7 +167,9 @@ def __init__(self, left: Cycler[K, V] | Iterable[dict[K,V]] | None, right: Cycle self._left = None if isinstance(right, Cycler): - self._right: Cycler[K, V] | list[dict[K,V]] | None = Cycler(right._left, right._right, right._op) + self._right: Cycler[K, V] | list[dict[K, V]] | None = Cycler( + right._left, right._right, right._op + ) elif right is not None: # Need to copy the dictionary or else that will be a residual # mutable that could lead to strange errors @@ -185,22 +201,31 @@ def change_key(self, old: K, new: K) -> None: return if new in self._keys: raise ValueError( - "Can't replace {old} with {new}, {new} is already a key" - .format(old=old, new=new) + "Can't replace {old} with {new}, {new} is already a key".format( + old=old, new=new + ) ) if old not in self._keys: - raise KeyError("Can't replace {old} with {new}, {old} is not a key" - .format(old=old, new=new)) + raise KeyError( + "Can't replace {old} with {new}, {old} is not a key".format( + old=old, new=new + ) + ) self._keys.remove(old) self._keys.add(new) - if self._right is not None and isinstance(self._right, Cycler) and old in self._right.keys: + if ( + self._right is not None + and isinstance(self._right, Cycler) + and old in self._right.keys + ): self._right.change_key(old, new) # self._left should always be non-None # if self._keys is non-empty. - elif self._left is None: pass + elif self._left is None: + pass elif isinstance(self._left, Cycler): self._left.change_key(old, new) else: @@ -249,7 +274,9 @@ def __iter__(self) -> Generator[dict[K, V], None, None]: yield dict(left) else: if self._op is None: - raise TypeError("Operation cannot be None when both left and right are defined") + raise TypeError( + "Operation cannot be None when both left and right are defined" + ) for a, b in self._op(self._left, self._right): out = {} out.update(a) @@ -265,8 +292,9 @@ def __add__(self, other: Cycler[K, V]) -> Cycler[K, V]: other : Cycler """ if len(self) != len(other): - raise ValueError("Can only add equal length cycles, " - f"not {len(self)} and {len(other)}") + raise ValueError( + f"Can only add equal length cycles, not {len(self)} and {len(other)}" + ) return Cycler(self, other, zip) def __mul__(self, other: Cycler[K, V] | int) -> Cycler[K, V]: @@ -282,7 +310,7 @@ def __mul__(self, other: Cycler[K, V] | int) -> Cycler[K, V]: return Cycler(self, other, product) elif isinstance(other, int): trans = self.by_key() - return reduce(add, (_cycler(k, v*other) for k, v in trans.items())) + return reduce(add, (_cycler(k, v * other) for k, v in trans.items())) else: return NotImplemented @@ -319,7 +347,7 @@ def __iadd__(self, other: Cycler[K, V]) -> Cycler[K, V]: self._right = Cycler(other._left, other._right, other._op) return self - def __imul__(self, other: Cycler[K,V]|int) -> Cycler[K, V]: + def __imul__(self, other: Cycler[K, V] | int) -> Cycler[K, V]: """ In-place outer product of two cyclers (`itertools.product`). @@ -349,13 +377,13 @@ def __eq__(self, other: object) -> bool: __hash__ = None # type: ignore def __repr__(self) -> str: - op_map = {zip: '+', product: '*'} + op_map = {zip: "+", product: "*"} if self._right is None: lab = self.keys.pop() itr = list(v[lab] for v in self) return f"cycler({lab!r}, {itr!r})" else: - op = op_map.get(self._op, '?') + op = op_map.get(self._op, "?") msg = "({left!r} {op} {right!r})" return msg.format(left=self._left, op=op, right=self._right) @@ -423,12 +451,22 @@ def simplify(self) -> Cycler[K, V]: concat = concat + @overload -def cycler(args: Cycler[K, V]) -> Cycler[K, V]: ... +def cycler(args: Cycler[K, V]) -> Cycler[K, V]: + ... + + @overload -def cycler(**kwargs: Iterable[V]) -> Cycler[str, V]: ... +def cycler(**kwargs: Iterable[V]) -> Cycler[str, V]: + ... + + @overload -def cycler(label: K, itr: Iterable[V]) -> Cycler[K, V]: ... +def cycler(label: K, itr: Iterable[V]) -> Cycler[K, V]: + ... + + def cycler(*args, **kwargs): """ Create a new `Cycler` object from a single positional argument, @@ -468,19 +506,24 @@ def cycler(*args, **kwargs): """ if args and kwargs: - raise TypeError("cyl() can only accept positional OR keyword " - "arguments -- not both.") + raise TypeError( + "cyl() can only accept positional OR keyword arguments -- not both." + ) if len(args) == 1: if not isinstance(args[0], Cycler): - raise TypeError("If only one positional argument given, it must " - "be a Cycler instance.") + raise TypeError( + "If only one positional argument given, it must " + "be a Cycler instance." + ) return Cycler(args[0]) elif len(args) == 2: return _cycler(*args) elif len(args) > 2: - raise TypeError("Only a single Cycler can be accepted as the lone " - "positional argument. Use keyword arguments instead.") + raise TypeError( + "Only a single Cycler can be accepted as the lone " + "positional argument. Use keyword arguments instead." + ) if kwargs: return reduce(add, (_cycler(k, v) for k, v in kwargs.items())) From b696d81c469dd7ce81f2139c8fc6479f49640e90 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 8 Dec 2022 00:05:45 -0600 Subject: [PATCH 61/98] Flake8 warnings --- cycler/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index 3efb050..f27e347 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -43,7 +43,7 @@ from __future__ import annotations -from collections.abc import Hashable, Iterable, Callable +from collections.abc import Hashable, Iterable import copy from functools import reduce from itertools import product, cycle @@ -201,15 +201,11 @@ def change_key(self, old: K, new: K) -> None: return if new in self._keys: raise ValueError( - "Can't replace {old} with {new}, {new} is already a key".format( - old=old, new=new - ) + f"Can't replace {old} with {new}, {new} is already a key" ) if old not in self._keys: raise KeyError( - "Can't replace {old} with {new}, {old} is not a key".format( - old=old, new=new - ) + f"Can't replace {old} with {new}, {old} is not a key" ) self._keys.remove(old) @@ -310,7 +306,9 @@ def __mul__(self, other: Cycler[K, V] | int) -> Cycler[K, V]: return Cycler(self, other, product) elif isinstance(other, int): trans = self.by_key() - return reduce(add, (_cycler(k, v * other) for k, v in trans.items())) + return reduce( + add, (_cycler(k, v * other) for k, v in trans.items()) + ) else: return NotImplemented From c5263bf0e0d463cad8ca12e5edcfc0afbe9d7c98 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 8 Dec 2022 00:06:39 -0600 Subject: [PATCH 62/98] Limit scope of flake8 workflow to main library, not setup.py --- .github/workflows/reviewdog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index 1137693..4fb399d 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -30,6 +30,6 @@ jobs: REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -o pipefail - flake8 | \ + flake8 --max-line-length 88 cycler | \ reviewdog -f=pep8 -name=flake8 \ -tee -reporter=github-check -filter-mode nofilter From 40aea83a13b656554880e7ef0b5aa267b97ceca1 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 8 Dec 2022 00:11:58 -0600 Subject: [PATCH 63/98] type ignore pytest --- test_cycler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_cycler.py b/test_cycler.py index a9130ff..c771e89 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -2,7 +2,7 @@ from operator import add, iadd, mul, imul from itertools import product, cycle, chain -import pytest +import pytest # type: ignore from cycler import cycler, Cycler, concat From a6b5a346098cbb94fabfeb964d2f35f8176ca492 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 29 Mar 2023 15:41:13 -0400 Subject: [PATCH 64/98] DOC: remove travis shield RIP travis.ci --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index 38bce53..6b18afc 100644 --- a/README.rst +++ b/README.rst @@ -6,8 +6,6 @@ .. |Supported Python versions| image:: https://img.shields.io/pypi/pyversions/cycler.svg .. _Supported Python versions: https://pypi.python.org/pypi/cycler -.. |Travis| image:: https://travis-ci.org/matplotlib/cycler.svg?branch=master -.. _Travis: https://travis-ci.org/matplotlib/cycler .. |Codecov| image:: https://codecov.io/github/matplotlib/cycler/badge.svg?branch=master&service=github .. _Codecov: https://codecov.io/github/matplotlib/cycler?branch=master From 7412cb2ab13617310a87fa0dd824ebdff882920a Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 31 Mar 2023 21:44:39 -0500 Subject: [PATCH 65/98] Move flake8 config to .flake8 --- .flake8 | 26 ++++++++++++++++++++++++++ .github/workflows/reviewdog.yml | 2 +- setup.py | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..407d802 --- /dev/null +++ b/.flake8 @@ -0,0 +1,26 @@ +[flake8] +max-line-length = 88 +select = + # flake8 default + D, E, F, W, +ignore = + # flake8 default + E121,E123,E126,E226,E24,E704,W503,W504, + # pydocstyle + D100, D101, D102, D103, D104, D105, D106, + D200, D202, D204, D205, + D301, + D400, D401, D403, D404 + # ignored by pydocstyle numpy docstring convention + D107, D203, D212, D213, D402, D413, D415, D416, D417, + +exclude = + .git + build + # External files. + .tox + .eggs + +per-file-ignores = + setup.py: E402 +force-check = True diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index 4fb399d..1137693 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -30,6 +30,6 @@ jobs: REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -o pipefail - flake8 --max-line-length 88 cycler | \ + flake8 | \ reviewdog -f=pep8 -name=flake8 \ -tee -reporter=github-check -filter-mode nofilter diff --git a/setup.py b/setup.py index bc5880d..0498166 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ author='Thomas A Caswell', author_email='matplotlib-users@python.org', packages=find_packages(), - package_data = {"cycler": ["py.typed"]}, + package_data={"cycler": ["py.typed"]}, description='Composable style cycles', url='https://github.com/matplotlib/cycler', platforms='Cross platform (Linux, macOS, Windows)', From 52506c6f3d3e447d63825213c1f3ef434bc56a95 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Mon, 3 Apr 2023 23:40:07 -0500 Subject: [PATCH 66/98] Disallow left of None, properly account for adding/multiplying cyclers with different types py39 compat py38 compat flake8 --- cycler/__init__.py | 82 +++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index f27e347..60a83db 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -43,21 +43,24 @@ from __future__ import annotations -from collections.abc import Hashable, Iterable +from collections.abc import Hashable, Iterable, Generator import copy from functools import reduce from itertools import product, cycle from operator import mul, add -from typing import TypeVar, Generic, Generator, Any, overload +# Dict, List, Union required for runtime cast calls +from typing import TypeVar, Generic, Callable, Union, Dict, List, Any, overload, cast __version__ = "0.12.0.dev0" K = TypeVar("K", bound=Hashable) +L = TypeVar("L", bound=Hashable) V = TypeVar("V") +U = TypeVar("U") def _process_keys( - left: Cycler[K, V] | Iterable[dict[K, V]] | None, + left: Cycler[K, V] | Iterable[dict[K, V]], right: Cycler[K, V] | Iterable[dict[K, V]] | None, ) -> set[K]: """ @@ -73,7 +76,7 @@ def _process_keys( keys : set The keys in the composition of the two cyclers. """ - l_peek: dict[K, V] = next(iter(left)) if left is not None else {} + l_peek: dict[K, V] = next(iter(left)) if left != [] else {} r_peek: dict[K, V] = next(iter(right)) if right is not None else {} l_key: set[K] = set(l_peek.keys()) r_key: set[K] = set(r_peek.keys()) @@ -82,7 +85,7 @@ def _process_keys( return l_key | r_key -def concat(left: Cycler[K, V], right: Cycler[K, V]) -> Cycler[K, V]: +def concat(left: Cycler[K, V], right: Cycler[K, U]) -> Cycler[K, V | U]: r""" Concatenate `Cycler`\s, as if chained using `itertools.chain`. @@ -108,8 +111,8 @@ def concat(left: Cycler[K, V], right: Cycler[K, V]) -> Cycler[K, V]: both=left.keys & right.keys, just_one=left.keys ^ right.keys ) ) - _l = left.by_key() - _r = right.by_key() + _l = cast(Dict[K, List[Union[V, U]]], left.by_key()) + _r = cast(Dict[K, List[Union[V, U]]], right.by_key()) return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys)) @@ -156,7 +159,7 @@ def __init__( Do not use this directly, use `cycler` function instead. """ if isinstance(left, Cycler): - self._left: Cycler[K, V] | list[dict[K, V]] | None = Cycler( + self._left: Cycler[K, V] | list[dict[K, V]] = Cycler( left._left, left._right, left._op ) elif left is not None: @@ -164,7 +167,7 @@ def __init__( # mutable that could lead to strange errors self._left = [copy.copy(v) for v in left] else: - self._left = None + self._left = [] if isinstance(right, Cycler): self._right: Cycler[K, V] | list[dict[K, V]] | None = Cycler( @@ -220,8 +223,6 @@ def change_key(self, old: K, new: K) -> None: # self._left should always be non-None # if self._keys is non-empty. - elif self._left is None: - pass elif isinstance(self._left, Cycler): self._left.change_key(old, new) else: @@ -264,10 +265,9 @@ def __getitem__(self, key: slice) -> Cycler[K, V]: raise ValueError("Can only use slices with Cycler.__getitem__") def __iter__(self) -> Generator[dict[K, V], None, None]: - if self._right is None or self._left is None: - if self._left is not None: - for left in self._left: - yield dict(left) + if self._right is None: + for left in self._left: + yield dict(left) else: if self._op is None: raise TypeError( @@ -279,7 +279,7 @@ def __iter__(self) -> Generator[dict[K, V], None, None]: out.update(b) yield out - def __add__(self, other: Cycler[K, V]) -> Cycler[K, V]: + def __add__(self, other: Cycler[L, U]) -> Cycler[K | L, V | U]: """ Pair-wise combine two equal length cyclers (zip). @@ -291,9 +291,21 @@ def __add__(self, other: Cycler[K, V]) -> Cycler[K, V]: raise ValueError( f"Can only add equal length cycles, not {len(self)} and {len(other)}" ) - return Cycler(self, other, zip) + return Cycler( + cast(Cycler[Union[K, L], Union[V, U]], self), + cast(Cycler[Union[K, L], Union[V, U]], other), + zip + ) + + @overload + def __mul__(self, other: Cycler[L, U]) -> Cycler[K | L, V | U]: + ... + + @overload + def __mul__(self, other: int) -> Cycler[K, V]: + ... - def __mul__(self, other: Cycler[K, V] | int) -> Cycler[K, V]: + def __mul__(self, other): """ Outer product of two cyclers (`itertools.product`) or integer multiplication. @@ -303,7 +315,11 @@ def __mul__(self, other: Cycler[K, V] | int) -> Cycler[K, V]: other : Cycler or int """ if isinstance(other, Cycler): - return Cycler(self, other, product) + return Cycler( + cast(Cycler[Union[K, L], Union[V, U]], self), + cast(Cycler[Union[K, L], Union[V, U]], other), + product + ) elif isinstance(other, int): trans = self.by_key() return reduce( @@ -312,22 +328,28 @@ def __mul__(self, other: Cycler[K, V] | int) -> Cycler[K, V]: else: return NotImplemented - def __rmul__(self, other: Cycler[K, V]) -> Cycler[K, V]: + @overload + def __rmul__(self, other: Cycler[L, U]) -> Cycler[K | L, V | U]: + ... + + @overload + def __rmul__(self, other: int) -> Cycler[K, V]: + ... + + def __rmul__(self, other): return self * other def __len__(self) -> int: - op_dict = {zip: min, product: mul} - if self._left is None: - if self._left is None: - return 0 - return 0 + op_dict: dict[Callable, Callable[[int, int], int]] = {zip: min, product: mul} if self._right is None: return len(self._left) l_len = len(self._left) r_len = len(self._right) - return op_dict[self._op](l_len, r_len) # type: ignore + return op_dict[self._op](l_len, r_len) - def __iadd__(self, other: Cycler[K, V]) -> Cycler[K, V]: + # iadd and imul do not exapand the the type as the returns must be consistent with + # self, thus they flag as inconsistent with add/mul + def __iadd__(self, other: Cycler[K, V]) -> Cycler[K, V]: # type: ignore[misc] """ In-place pair-wise combine two equal length cyclers (zip). @@ -345,7 +367,7 @@ def __iadd__(self, other: Cycler[K, V]) -> Cycler[K, V]: self._right = Cycler(other._left, other._right, other._op) return self - def __imul__(self, other: Cycler[K, V] | int) -> Cycler[K, V]: + def __imul__(self, other: Cycler[K, V] | int) -> Cycler[K, V]: # type: ignore[misc] """ In-place outer product of two cyclers (`itertools.product`). @@ -451,7 +473,7 @@ def simplify(self) -> Cycler[K, V]: @overload -def cycler(args: Cycler[K, V]) -> Cycler[K, V]: +def cycler(arg: Cycler[K, V]) -> Cycler[K, V]: ... @@ -505,7 +527,7 @@ def cycler(*args, **kwargs): """ if args and kwargs: raise TypeError( - "cyl() can only accept positional OR keyword arguments -- not both." + "cycler() can only accept positional OR keyword arguments -- not both." ) if len(args) == 1: From d19e475d6fad580add2b4de16a1000bf18f1da6c Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Wed, 5 Apr 2023 17:27:05 -0500 Subject: [PATCH 67/98] Disallow right child from being iterator of dict --- cycler/__init__.py | 14 +++----------- test_cycler.py | 4 ++-- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index 60a83db..b0a1562 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -150,7 +150,7 @@ def __call__(self): def __init__( self, left: Cycler[K, V] | Iterable[dict[K, V]] | None, - right: Cycler[K, V] | Iterable[dict[K, V]] | None = None, + right: Cycler[K, V] | None = None, op: Any = None, ): """ @@ -170,13 +170,9 @@ def __init__( self._left = [] if isinstance(right, Cycler): - self._right: Cycler[K, V] | list[dict[K, V]] | None = Cycler( + self._right: Cycler[K, V] | None = Cycler( right._left, right._right, right._op ) - elif right is not None: - # Need to copy the dictionary or else that will be a residual - # mutable that could lead to strange errors - self._right = [copy.copy(v) for v in right] else: self._right = None @@ -214,11 +210,7 @@ def change_key(self, old: K, new: K) -> None: self._keys.remove(old) self._keys.add(new) - if ( - self._right is not None - and isinstance(self._right, Cycler) - and old in self._right.keys - ): + if self._right is not None and old in self._right.keys: self._right.change_key(old, new) # self._left should always be non-None diff --git a/test_cycler.py b/test_cycler.py index c771e89..637521b 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -289,10 +289,10 @@ def test_cycler_exceptions(): pytest.raises(TypeError, cycler, 'c', 'rgb', 'lw', range(3)) -def test_starange_init(): +def test_strange_init(): c = cycler('r', 'rgb') c2 = cycler('lw', range(3)) - cy = Cycler(list(c), list(c2), zip) + cy = Cycler(list(c), c2, zip) assert cy == c + c2 From d5cae28e989fa9c3509f03ae51f8df840e88a6c7 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 11 Nov 2021 03:25:07 -0500 Subject: [PATCH 68/98] Modernize setuptools config It's moved to a more declarative syntax over putting everything in a function call. --- .github/workflows/mypy.yml | 2 +- .github/workflows/tests.yml | 2 +- MANIFEST.in | 5 ----- pyproject.toml | 33 +++++++++++++++++++++++++++++++++ setup.cfg | 2 -- setup.py | 25 ------------------------- 6 files changed, 35 insertions(+), 34 deletions(-) delete mode 100644 MANIFEST.in create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 55d87a4..d29778a 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -24,7 +24,7 @@ jobs: - name: Install Python dependencies run: | - python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade pip python -m pip install --upgrade mypy - name: Install cycler diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 695f814..766fb23 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,7 +30,7 @@ jobs: - name: Install Python dependencies run: | - python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade pip python -m pip install --upgrade pytest pytest-cov pytest-xdist - name: Install cycler diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index e6bfad6..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include test_cycler.py -include LICENSE -recursive-include doc Makefile make.bat *.rst *.py -exclude appveyor.yml -recursive-exclude .github * diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..711945a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "cycler" +version = "0.12.0.dev0" +description = "Composable style cycles" +authors = [ + {name = "Thomas A Caswell", email = "matplotlib-users@python.org"}, +] +readme = "README.rst" +license = {file = "LICENSE"} +requires-python = ">=3.7" +classifiers = [ + "License :: OSI Approved :: BSD License", + "Development Status :: 4 - Beta", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", +] +keywords = ["cycle kwargs"] + +[project.urls] +homepage = "https://matplotlib.org/cycler/" +repository = "https://github.com/matplotlib/cycler" + +[tool.setuptools] +packages = ["cycler"] + +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 0c9e0fc..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -license_file = LICENSE diff --git a/setup.py b/setup.py deleted file mode 100644 index 0498166..0000000 --- a/setup.py +++ /dev/null @@ -1,25 +0,0 @@ -from setuptools import setup, find_packages - -setup(name='cycler', - version='0.12.0.dev0', - author='Thomas A Caswell', - author_email='matplotlib-users@python.org', - packages=find_packages(), - package_data={"cycler": ["py.typed"]}, - description='Composable style cycles', - url='https://github.com/matplotlib/cycler', - platforms='Cross platform (Linux, macOS, Windows)', - license="BSD", - python_requires='>=3.7', - classifiers=['License :: OSI Approved :: BSD License', - 'Development Status :: 4 - Beta', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3 :: Only', - ], - keywords='cycle kwargs', - ) From b41550f21bc3588a654340b310f86a0cd18e820b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 13 Apr 2023 19:42:48 -0400 Subject: [PATCH 69/98] Make project version metadata dynamic This reduces the places one needs to update a version string. --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 711945a..fb7e74c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cycler" -version = "0.12.0.dev0" +dynamic = ["version"] description = "Composable style cycles" authors = [ {name = "Thomas A Caswell", email = "matplotlib-users@python.org"}, @@ -28,6 +28,9 @@ repository = "https://github.com/matplotlib/cycler" [tool.setuptools] packages = ["cycler"] +[tool.setuptools.dynamic] +version = {attr = "cycler.__version__"} + [build-system] requires = ["setuptools>=61"] build-backend = "setuptools.build_meta" From 7bd24b26a8bb5f223e7db921a78ea50d923a517b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 13 Apr 2023 19:55:53 -0400 Subject: [PATCH 70/98] doc: Generate version from installed cycler --- doc/source/conf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index d58c79b..cb28171 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -65,10 +65,10 @@ # |version| and |release|, also used in various other places throughout the # built documents. # -# The short X.Y version. -version = '0.12.0' # The full version, including alpha/beta/rc tags. -release = '0.12.0.dev0' +from cycler import __version__ as release # noqa +# The short X.Y version. +version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 2c301290ae78e47d9ca1349cca420496509bcaac Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 13 Apr 2023 19:04:37 -0400 Subject: [PATCH 71/98] Post mypy results to PR review This uses reviewdog, similar to on the main Matplotlib repo. --- .github/workflows/mypy.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index d29778a..e07774b 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -22,6 +22,14 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 + - name: Set up reviewdog + run: | + mkdir -p "$HOME/bin" + curl -sfL \ + https://github.com/reviewdog/reviewdog/raw/master/install.sh | \ + sh -s -- -b "$HOME/bin" + echo "$HOME/bin" >> $GITHUB_PATH + - name: Install Python dependencies run: | python -m pip install --upgrade pip @@ -29,8 +37,12 @@ jobs: - name: Install cycler run: | - python -m pip install --no-deps . + python -m pip install --no-deps -e . - name: Run mypy + env: + REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - mypy cycler test_cycler.py + mypy cycler test_cycler.py | \ + reviewdog -f=mypy -name=mypy \ + -tee -reporter=github-check -filter-mode nofilter From d3bc9a8df98569d03c2ffa6297cc38facc3993fd Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 13 Apr 2023 19:26:02 -0400 Subject: [PATCH 72/98] Merge mypy workflow into linting workflow --- .github/workflows/mypy.yml | 48 --------------------------------- .github/workflows/reviewdog.yml | 44 +++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 49 deletions(-) delete mode 100644 .github/workflows/mypy.yml diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml deleted file mode 100644 index e07774b..0000000 --- a/.github/workflows/mypy.yml +++ /dev/null @@ -1,48 +0,0 @@ ---- - -name: Mypy - -on: - push: - branches-ignore: - - auto-backport-of-pr-[0-9]+ - - v[0-9]+.[0-9]+.[0-9x]+-doc - pull_request: - -jobs: - test: - name: "Mypy" - runs-on: ubuntu-20.04 - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v4 - - - name: Set up reviewdog - run: | - mkdir -p "$HOME/bin" - curl -sfL \ - https://github.com/reviewdog/reviewdog/raw/master/install.sh | \ - sh -s -- -b "$HOME/bin" - echo "$HOME/bin" >> $GITHUB_PATH - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade mypy - - - name: Install cycler - run: | - python -m pip install --no-deps -e . - - - name: Run mypy - env: - REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - mypy cycler test_cycler.py | \ - reviewdog -f=mypy -name=mypy \ - -tee -reporter=github-check -filter-mode nofilter diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index 1137693..8590fc7 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -1,6 +1,11 @@ --- name: Linting -on: [pull_request] +on: + push: + branches-ignore: + - auto-backport-of-pr-[0-9]+ + - v[0-9]+.[0-9]+.[0-9x]+-doc + pull_request: jobs: flake8: @@ -33,3 +38,40 @@ jobs: flake8 | \ reviewdog -f=pep8 -name=flake8 \ -tee -reporter=github-check -filter-mode nofilter + + mypy: + name: "Mypy" + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + + - name: Set up reviewdog + run: | + mkdir -p "$HOME/bin" + curl -sfL \ + https://github.com/reviewdog/reviewdog/raw/master/install.sh | \ + sh -s -- -b "$HOME/bin" + echo "$HOME/bin" >> $GITHUB_PATH + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade mypy + + - name: Install cycler + run: | + python -m pip install --no-deps -e . + + - name: Run mypy + env: + REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mypy cycler test_cycler.py | \ + reviewdog -f=mypy -name=mypy \ + -tee -reporter=github-check -filter-mode nofilter From f20e0f53d0eb656e21393d4e1540d1dce705ef3f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 14 Apr 2023 17:40:52 -0400 Subject: [PATCH 73/98] Pin mypy to current version --- .github/workflows/reviewdog.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index 8590fc7..9b26c2b 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -62,7 +62,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - python -m pip install --upgrade mypy + python -m pip install --upgrade mypy==1.2.0 - name: Install cycler run: | From 47dbdf71042bec0107e53c11f8d578a8bc537f5c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 16 Sep 2023 17:22:51 +0300 Subject: [PATCH 74/98] Add support for Python 3.12 --- .github/workflows/reviewdog.yml | 4 ++-- .github/workflows/tests.yml | 5 +++-- pyproject.toml | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index 9b26c2b..d70c14c 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -12,7 +12,7 @@ jobs: name: flake8 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3 uses: actions/setup-python@v4 @@ -44,7 +44,7 @@ jobs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 766fb23..b52496c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,10 +16,10 @@ jobs: strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -27,6 +27,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install Python dependencies run: | diff --git a/pyproject.toml b/pyproject.toml index fb7e74c..03552c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", ] keywords = ["cycle kwargs"] From 9b4800e07788cd18f0a3e342ae616d4e0a74c4af Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 16 Sep 2023 17:23:58 +0300 Subject: [PATCH 75/98] Drop support for EOL Python 3.7 --- .github/workflows/tests.yml | 2 +- appveyor.yml | 4 ---- pyproject.toml | 3 +-- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b52496c..88e0936 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/appveyor.yml b/appveyor.yml index a981d68..57ca08c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,10 +5,6 @@ image: Visual Studio 2019 environment: matrix: - - PYTHON: "C:\\Python36" - - PYTHON: "C:\\Python36-x64" - - PYTHON: "C:\\Python37" - - PYTHON: "C:\\Python37-x64" - PYTHON: "C:\\Python38" - PYTHON: "C:\\Python38-x64" - PYTHON: "C:\\Python39" diff --git a/pyproject.toml b/pyproject.toml index 03552c5..11f2a7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,12 +7,11 @@ authors = [ ] readme = "README.rst" license = {file = "LICENSE"} -requires-python = ">=3.7" +requires-python = ">=3.8" classifiers = [ "License :: OSI Approved :: BSD License", "Development Status :: 4 - Beta", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", From a267c52e8703e924d73aac21feb59173834c4932 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Mon, 18 Sep 2023 12:05:13 +0200 Subject: [PATCH 76/98] Bump mypy to 1.5.1 --- .github/workflows/reviewdog.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index d70c14c..ce95577 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -50,6 +50,8 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 + with: + python-version: 3.9 - name: Set up reviewdog run: | @@ -62,7 +64,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - python -m pip install --upgrade mypy==1.2.0 + python -m pip install --upgrade mypy==1.5.1 - name: Install cycler run: | From 254fa765ad297996359ca64cbdf030b5870a2a08 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Mon, 18 Sep 2023 11:46:00 +0200 Subject: [PATCH 77/98] Update badges --- README.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 6b18afc..2304031 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,19 @@ -|PyPi|_ |Supported Python versions|_ |Travis|_ |Codecov|_ +|PyPi|_ |Conda|_ |Supported Python versions|_ |Github Actions|_ |Codecov|_ .. |PyPi| image:: https://img.shields.io/pypi/v/cycler.svg?style=flat .. _PyPi: https://pypi.python.org/pypi/cycler +.. |Conda| image:: https://img.shields.io/conda/v/conda-forge/cycler +.. _Conda: https://anaconda.org/conda-forge/cycler + .. |Supported Python versions| image:: https://img.shields.io/pypi/pyversions/cycler.svg .. _Supported Python versions: https://pypi.python.org/pypi/cycler +.. |Github Actions| iamge:: https://github.com/matplotlib/cycler/actions/workflows/tests.yml/badge.svg +.. _Github Actions: https://github.com/matplotlib/cycler/actions -.. |Codecov| image:: https://codecov.io/github/matplotlib/cycler/badge.svg?branch=master&service=github -.. _Codecov: https://codecov.io/github/matplotlib/cycler?branch=master +.. |Codecov| image:: https://codecov.io/github/matplotlib/cycler/badge.svg?branch=main&service=github +.. _Codecov: https://codecov.io/github/matplotlib/cycler?branch=main cycler: composable cycles ========================= From 3c4b1948dc53ee1d8bc6d060c64ebe5acd0e7bf7 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 10 Nov 2021 23:24:27 -0500 Subject: [PATCH 78/98] Fix minor doc typos --- doc/source/index.rst | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/doc/source/index.rst b/doc/source/index.rst index f262c17..126de4b 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -40,10 +40,10 @@ composition and iteration logic. Base ---- -A single entry `Cycler` object can be used to easily -cycle over a single style. To create the `Cycler` use the :py:func:`cycler` -function to link a key/style/kwarg to series of values. The key must be -hashable (as it will eventually be used as the key in a :obj:`dict`). +A single entry `Cycler` object can be used to easily cycle over a single style. +To create the `Cycler` use the :py:func:`cycler` function to link a +key/style/keyword argument to series of values. The key must be hashable (as it +will eventually be used as the key in a :obj:`dict`). .. ipython:: python @@ -53,7 +53,7 @@ hashable (as it will eventually be used as the key in a :obj:`dict`). color_cycle = cycler(color=['r', 'g', 'b']) color_cycle -The `Cycler` knows it's length and keys: +The `Cycler` knows its length and keys: .. ipython:: python @@ -97,7 +97,7 @@ create complex multi-key cycles. Addition ~~~~~~~~ -Equal length `Cycler` s with different keys can be added to get the +Equal length `Cycler`\s with different keys can be added to get the 'inner' product of two cycles .. ipython:: python @@ -180,7 +180,7 @@ matrices) Integer Multiplication ~~~~~~~~~~~~~~~~~~~~~~ -`Cycler` s can also be multiplied by integer values to increase the length. +`Cycler`\s can also be multiplied by integer values to increase the length. .. ipython:: python @@ -331,8 +331,7 @@ the same style. Exceptions ---------- - -A :obj:`ValueError` is raised if unequal length `Cycler` s are added together +A :obj:`ValueError` is raised if unequal length `Cycler`\s are added together .. ipython:: python :okexcept: @@ -401,6 +400,6 @@ However, if you want to do something more complicated: ax.legend(loc=0) -the plotting logic can quickly become very involved. To address this and allow easy -cycling over arbitrary ``kwargs`` the `Cycler` class, a composable -kwarg iterator, was developed. +the plotting logic can quickly become very involved. To address this and allow +easy cycling over arbitrary ``kwargs`` the `Cycler` class, a composable keyword +argument iterator, was developed. From 565769ff1c58da761148f9754bd8a1039d9c5b22 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 10 Nov 2021 23:32:57 -0500 Subject: [PATCH 79/98] DOC: Remove empty static path setting --- doc/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index cb28171..906a782 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -142,7 +142,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = [] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied From 1426f5f83a6e60b9e1092dcc5f96f8494d2f8d85 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 13 Apr 2023 19:34:45 -0400 Subject: [PATCH 80/98] Add test and doc requirements to pyproject --- .github/workflows/tests.yml | 5 ++--- pyproject.toml | 13 +++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 88e0936..98ceb87 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,14 +29,13 @@ jobs: python-version: ${{ matrix.python-version }} allow-prereleases: true - - name: Install Python dependencies + - name: Upgrade pip run: | python -m pip install --upgrade pip - python -m pip install --upgrade pytest pytest-cov pytest-xdist - name: Install cycler run: | - python -m pip install --no-deps . + python -m pip install .[tests] - name: Run pytest run: | diff --git a/pyproject.toml b/pyproject.toml index 11f2a7c..cda6787 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,19 @@ keywords = ["cycle kwargs"] homepage = "https://matplotlib.org/cycler/" repository = "https://github.com/matplotlib/cycler" +[project.optional-dependencies] +docs = [ + "ipython", + "matplotlib", + "numpydoc", + "sphinx", +] +tests = [ + "pytest", + "pytest-cov", + "pytest-xdist", +] + [tool.setuptools] packages = ["cycler"] From 060ec33b9711b06b5915f855771c6280cbf577bf Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 13 Apr 2023 20:18:01 -0400 Subject: [PATCH 81/98] Add Circle CI job for doc review --- .circleci/config.yml | 127 +++++++++++++++++++++++++++++++++ .circleci/fetch_doc_logs.py | 66 +++++++++++++++++ .github/workflows/circleci.yml | 58 +++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 .circleci/fetch_doc_logs.py create mode 100644 .github/workflows/circleci.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..242d022 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,127 @@ +# Circle CI configuration file +# https://circleci.com/docs/ + +--- +version: 2.1 + +####################################### +# Define some common steps as commands. +# + +commands: + check-skip: + steps: + - run: + name: Check-skip + command: | + export git_log=$(git log --max-count=1 --pretty=format:"%B" | + tr "\n" " ") + echo "Got commit message:" + echo "${git_log}" + if [[ -v CIRCLE_PULL_REQUEST ]] && ( \ + [[ "$git_log" == *"[skip circle]"* ]] || \ + [[ "$git_log" == *"[circle skip]"* ]]); then + echo "Skip detected, exiting job ${CIRCLE_JOB} for PR ${CIRCLE_PULL_REQUEST}." + circleci-agent step halt; + fi + + merge: + steps: + - run: + name: Merge with upstream + command: | + if ! git remote -v | grep upstream; then + git remote add upstream https://github.com/matplotlib/cycler.git + fi + git fetch upstream + if [[ "$CIRCLE_BRANCH" != "main" ]] && \ + [[ "$CIRCLE_PR_NUMBER" != "" ]]; then + echo "Merging ${CIRCLE_PR_NUMBER}" + git pull --ff-only upstream "refs/pull/${CIRCLE_PR_NUMBER}/merge" + fi + + pip-install: + description: Upgrade pip to get as clean an install as possible + steps: + - run: + name: Upgrade pip + command: | + python -m pip install --upgrade --user pip + + cycler-install: + steps: + - run: + name: Install Cycler + command: | + python -m pip install --user -ve .[docs] + + doc-build: + steps: + - restore_cache: + keys: + - sphinx-env-v1-{{ .BuildNum }}-{{ .Environment.CIRCLE_JOB }} + - sphinx-env-v1-{{ .Environment.CIRCLE_PREVIOUS_BUILD_NUM }}-{{ .Environment.CIRCLE_JOB }} + - run: + name: Build documentation + command: | + # Set epoch to date of latest tag. + export SOURCE_DATE_EPOCH="$(git log -1 --format=%at $(git describe --abbrev=0))" + mkdir -p logs + make html O="-T -j4 -w /tmp/sphinxerrorswarnings.log" + rm -r build/html/_sources + working_directory: doc + - save_cache: + key: sphinx-env-v1-{{ .BuildNum }}-{{ .Environment.CIRCLE_JOB }} + paths: + - doc/build/doctrees + + doc-show-errors-warnings: + steps: + - run: + name: Extract possible build errors and warnings + command: | + (grep "WARNING\|ERROR" /tmp/sphinxerrorswarnings.log || + echo "No errors or warnings") + # Save logs as an artifact, and convert from absolute paths to + # repository-relative paths. + sed "s~$PWD/~~" /tmp/sphinxerrorswarnings.log > \ + doc/logs/sphinx-errors-warnings.log + when: always + - store_artifacts: + path: doc/logs/sphinx-errors-warnings.log + +########################################## +# Here is where the real jobs are defined. +# + +jobs: + docs-python39: + docker: + - image: cimg/python:3.9 + resource_class: large + steps: + - checkout + - check-skip + - merge + + - pip-install + + - cycler-install + + - doc-build + - doc-show-errors-warnings + + - store_artifacts: + path: doc/build/html + +######################################### +# Defining workflows gets us parallelism. +# + +workflows: + version: 2 + build: + jobs: + # NOTE: If you rename this job, then you must update the `if` condition + # and `circleci-jobs` option in `.github/workflows/circleci.yml`. + - docs-python39 diff --git a/.circleci/fetch_doc_logs.py b/.circleci/fetch_doc_logs.py new file mode 100644 index 0000000..0a5552a --- /dev/null +++ b/.circleci/fetch_doc_logs.py @@ -0,0 +1,66 @@ +""" +Download artifacts from CircleCI for a documentation build. + +This is run by the :file:`.github/workflows/circleci.yml` workflow in order to +get the warning/deprecation logs that will be posted on commits as checks. Logs +are downloaded from the :file:`docs/logs` artifact path and placed in the +:file:`logs` directory. + +Additionally, the artifact count for a build is produced as a workflow output, +by appending to the file specified by :env:`GITHUB_OUTPUT`. + +If there are no logs, an "ERROR" message is printed, but this is not fatal, as +the initial 'status' workflow runs when the build has first started, and there +are naturally no artifacts at that point. + +This script should be run by passing the CircleCI build URL as its first +argument. In the GitHub Actions workflow, this URL comes from +``github.event.target_url``. +""" +import json +import os +from pathlib import Path +import sys +from urllib.parse import urlparse +from urllib.request import URLError, urlopen + + +if len(sys.argv) != 2: + print('USAGE: fetch_doc_results.py CircleCI-build-url') + sys.exit(1) + +target_url = urlparse(sys.argv[1]) +*_, organization, repository, build_id = target_url.path.split('/') +print(f'Fetching artifacts from {organization}/{repository} for {build_id}') + +artifact_url = ( + f'https://circleci.com/api/v2/project/gh/' + f'{organization}/{repository}/{build_id}/artifacts' +) +print(artifact_url) +try: + with urlopen(artifact_url) as response: + artifacts = json.load(response) +except URLError: + artifacts = {'items': []} +artifact_count = len(artifacts['items']) +print(f'Found {artifact_count} artifacts') + +with open(os.environ['GITHUB_OUTPUT'], 'w+') as fd: + fd.write(f'count={artifact_count}\n') + +logs = Path('logs') +logs.mkdir(exist_ok=True) + +found = False +for item in artifacts['items']: + path = item['path'] + if path.startswith('doc/logs/'): + path = Path(path).name + print(f'Downloading {path} from {item["url"]}') + with urlopen(item['url']) as response: + (logs / path).write_bytes(response.read()) + found = True + +if not found: + print('ERROR: Did not find any artifact logs!') diff --git a/.github/workflows/circleci.yml b/.github/workflows/circleci.yml new file mode 100644 index 0000000..384bc8e --- /dev/null +++ b/.github/workflows/circleci.yml @@ -0,0 +1,58 @@ +--- +name: "CircleCI artifact handling" +on: [status] +jobs: + circleci_artifacts_redirector_job: + if: "${{ github.event.context == 'ci/circleci: docs-python39' }}" + permissions: + statuses: write + runs-on: ubuntu-latest + name: Run CircleCI artifacts redirector + steps: + - name: GitHub Action step + uses: larsoner/circleci-artifacts-redirector-action@master + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + artifact-path: 0/doc/build/html/index.html + circleci-jobs: docs-python39 + job-title: View the built docs + + post_warnings_as_review: + if: "${{ github.event.context == 'ci/circleci: docs-python39' }}" + permissions: + contents: read + checks: write + pull-requests: write + runs-on: ubuntu-latest + name: Post warnings/errors as review + steps: + - uses: actions/checkout@v3 + + - name: Fetch result artifacts + id: fetch-artifacts + run: | + python .circleci/fetch_doc_logs.py "${{ github.event.target_url }}" + + - name: Set up reviewdog + if: "${{ steps.fetch-artifacts.outputs.count != 0 }}" + uses: reviewdog/action-setup@v1 + with: + reviewdog_version: latest + + - name: Post review + if: "${{ steps.fetch-artifacts.outputs.count != 0 }}" + env: + REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REVIEWDOG_SKIP_DOGHOUSE: "true" + CI_COMMIT: ${{ github.event.sha }} + CI_REPO_OWNER: ${{ github.event.repository.owner.login }} + CI_REPO_NAME: ${{ github.event.repository.name }} + run: | + # The 'status' event does not contain information in the way that + # reviewdog expects, so we unset those so it reads from the + # environment variables we set above. + unset GITHUB_ACTIONS GITHUB_EVENT_PATH + cat logs/sphinx-deprecations.log | \ + reviewdog \ + -efm '%f\:%l: %m' \ + -name=examples -tee -reporter=github-check -filter-mode=nofilter From 245c8866eb16c97ae0027458a65d9208dff99b8f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 18 Sep 2023 14:20:50 -0400 Subject: [PATCH 82/98] Add a release workflow to publish to PyPI This uses the new Trusted Publisher setup to automatically publish when a release is published. --- .github/workflows/release.yml | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..88954a9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +--- + +name: Release +on: + release: + types: + - published + +jobs: + build: + name: Build Release Packages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 10 + + - name: Set up Python + id: setup + uses: actions/setup-python@v4 + with: + python-version: 3.x + + - name: Install build tools + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade build + + - name: Build packages + run: python -m build + + - name: Save built packages as artifact + uses: actions/upload-artifact@v3 + with: + name: packages-${{ runner.os }}-${{ steps.setup.outputs.python-version }} + path: dist/ + if-no-files-found: error + retention-days: 5 + + publish: + name: Upload release to PyPI + needs: build + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + steps: + - name: Download packages + uses: actions/download-artifact@v3 + + - name: Consolidate packages for upload + run: | + mkdir dist + cp packages-*/* dist/ + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@b7f401de30cb6434a1e19f805ff006643653240e # v1.8.10 From 245170dfc926d66c7128f741ab37b6a501cc6bc9 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 18 Sep 2023 14:26:12 -0400 Subject: [PATCH 83/98] DOC: Fix typo in GitHub Actions badge --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 2304031..18712c9 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -|PyPi|_ |Conda|_ |Supported Python versions|_ |Github Actions|_ |Codecov|_ +|PyPi|_ |Conda|_ |Supported Python versions|_ |GitHub Actions|_ |Codecov|_ .. |PyPi| image:: https://img.shields.io/pypi/v/cycler.svg?style=flat .. _PyPi: https://pypi.python.org/pypi/cycler @@ -9,8 +9,8 @@ .. |Supported Python versions| image:: https://img.shields.io/pypi/pyversions/cycler.svg .. _Supported Python versions: https://pypi.python.org/pypi/cycler -.. |Github Actions| iamge:: https://github.com/matplotlib/cycler/actions/workflows/tests.yml/badge.svg -.. _Github Actions: https://github.com/matplotlib/cycler/actions +.. |GitHub Actions| image:: https://github.com/matplotlib/cycler/actions/workflows/tests.yml/badge.svg +.. _GitHub Actions: https://github.com/matplotlib/cycler/actions .. |Codecov| image:: https://codecov.io/github/matplotlib/cycler/badge.svg?branch=main&service=github .. _Codecov: https://codecov.io/github/matplotlib/cycler?branch=main From 74c2df2e55900f2e691425c00bc2b74a5ec3f1c2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 18 Sep 2023 17:27:26 -0400 Subject: [PATCH 84/98] CI: Use correct variable in CircleCI build --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 242d022..6a5c910 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,7 +67,7 @@ commands: # Set epoch to date of latest tag. export SOURCE_DATE_EPOCH="$(git log -1 --format=%at $(git describe --abbrev=0))" mkdir -p logs - make html O="-T -j4 -w /tmp/sphinxerrorswarnings.log" + make html SPHINXOPTS="-T -j4 -w /tmp/sphinxerrorswarnings.log" rm -r build/html/_sources working_directory: doc - save_cache: From b6f3e1748cbd7ea064bd9718d355f21b6c49fa58 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 27 Sep 2023 22:17:58 -0400 Subject: [PATCH 85/98] REL: v0.12.0rc1 This is the first release candidate for Cycler 0.12.0. The major new feature in this release is the addition of type hints. Furthermore, the minimum supported version of Python is now 3.8. --- cycler/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index b0a1562..5ae88d7 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -51,7 +51,7 @@ # Dict, List, Union required for runtime cast calls from typing import TypeVar, Generic, Callable, Union, Dict, List, Any, overload, cast -__version__ = "0.12.0.dev0" +__version__ = "0.12.0rc1" K = TypeVar("K", bound=Hashable) L = TypeVar("L", bound=Hashable) From ab3cfe8a2c7b873ede91dcaa3699b5c128446a11 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 27 Sep 2023 22:23:50 -0400 Subject: [PATCH 86/98] BLD: Bump version for next release --- cycler/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index 5ae88d7..792e928 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -51,7 +51,7 @@ # Dict, List, Union required for runtime cast calls from typing import TypeVar, Generic, Callable, Union, Dict, List, Any, overload, cast -__version__ = "0.12.0rc1" +__version__ = "0.12.0rc2" K = TypeVar("K", bound=Hashable) L = TypeVar("L", bound=Hashable) From 7aebbdfdf6935937a0e3e4d545eb486b845d5f24 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 28 Sep 2023 19:20:05 -0400 Subject: [PATCH 87/98] REL: 0.12.0 This is the first release of Cycler 0.12. The major new feature in this release is the addition of type hints. Furthermore, the minimum supported version of Python is now 3.8. --- cycler/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index 792e928..f412aef 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -51,7 +51,7 @@ # Dict, List, Union required for runtime cast calls from typing import TypeVar, Generic, Callable, Union, Dict, List, Any, overload, cast -__version__ = "0.12.0rc2" +__version__ = "0.12.0" K = TypeVar("K", bound=Hashable) L = TypeVar("L", bound=Hashable) From 99d0d4e37d86894cfed97715a7060fd27eea0cda Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 28 Sep 2023 19:30:11 -0400 Subject: [PATCH 88/98] BLD: Bump version for next release --- cycler/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index f412aef..ea37a2e 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -51,7 +51,7 @@ # Dict, List, Union required for runtime cast calls from typing import TypeVar, Generic, Callable, Union, Dict, List, Any, overload, cast -__version__ = "0.12.0" +__version__ = "0.13.0.dev0" K = TypeVar("K", bound=Hashable) L = TypeVar("L", bound=Hashable) From 720d663eb639d4f045785f9c4bdc010be83c9a33 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Mon, 2 Oct 2023 15:52:04 -0500 Subject: [PATCH 89/98] Include py.typed in exported wheels and sdists --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index cda6787..c7c79cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,9 @@ packages = ["cycler"] [tool.setuptools.dynamic] version = {attr = "cycler.__version__"} +[tool.setuptools.package-data] +cycler = ["py.typed"] + [build-system] requires = ["setuptools>=61"] build-backend = "setuptools.build_meta" From 6115d5c55d66cdedb67ec5a0d6b52b6df52fd9d2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 4 Oct 2023 02:36:28 -0400 Subject: [PATCH 90/98] REL: 0.12.1 This is the second release of Cycler 0.12. This fixes the previous release not shipping the `py.typed` file. --- cycler/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index ea37a2e..9794954 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -51,7 +51,7 @@ # Dict, List, Union required for runtime cast calls from typing import TypeVar, Generic, Callable, Union, Dict, List, Any, overload, cast -__version__ = "0.13.0.dev0" +__version__ = "0.12.1" K = TypeVar("K", bound=Hashable) L = TypeVar("L", bound=Hashable) From 6499cca6b8f21fb2c9860b27b79a50e2d3cfa512 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 7 Oct 2023 01:29:20 -0400 Subject: [PATCH 91/98] BLD: Bump version for next release --- cycler/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index 9794954..94b5f71 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -51,7 +51,8 @@ # Dict, List, Union required for runtime cast calls from typing import TypeVar, Generic, Callable, Union, Dict, List, Any, overload, cast -__version__ = "0.12.1" + +__version__ = "0.13.0.dev0" K = TypeVar("K", bound=Hashable) L = TypeVar("L", bound=Hashable) From 555b8bd47a8a64cd7dfb9f2bffd2d39792ce70f3 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Fri, 26 Jul 2024 15:49:13 -0400 Subject: [PATCH 92/98] [MNT] Update error message printed on overlapping cycles to be more verbose --- cycler/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index 94b5f71..ad40377 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -82,7 +82,8 @@ def _process_keys( l_key: set[K] = set(l_peek.keys()) r_key: set[K] = set(r_peek.keys()) if l_key & r_key: - raise ValueError("Can not compose overlapping cycles") + raise ValueError(f"Cannot compose overlapping cycles, duplicate key(s): {l_key & r_key}") + return l_key | r_key From da2514cc17c49c790e018e540e19a5601e1847dd Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Tue, 30 Jul 2024 09:34:51 -0400 Subject: [PATCH 93/98] Utilize walrus operator --- cycler/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index ad40377..02f50c7 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -81,8 +81,8 @@ def _process_keys( r_peek: dict[K, V] = next(iter(right)) if right is not None else {} l_key: set[K] = set(l_peek.keys()) r_key: set[K] = set(r_peek.keys()) - if l_key & r_key: - raise ValueError(f"Cannot compose overlapping cycles, duplicate key(s): {l_key & r_key}") + if common_keys := l_key & r_key: + raise ValueError(f"Cannot compose overlapping cycles, duplicate key(s): {common_keys}") return l_key | r_key From 2a858e4cb711322cc06c16043ad6146d07d2e64b Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Fri, 2 Aug 2024 13:25:45 -0400 Subject: [PATCH 94/98] Fix linting error --- cycler/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index 02f50c7..1f05670 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -82,7 +82,9 @@ def _process_keys( l_key: set[K] = set(l_peek.keys()) r_key: set[K] = set(r_peek.keys()) if common_keys := l_key & r_key: - raise ValueError(f"Cannot compose overlapping cycles, duplicate key(s): {common_keys}") + raise ValueError( + f"Cannot compose overlapping cycles, duplicate key(s): {common_keys}" + ) return l_key | r_key From ee4fc113bfc2ec5a70ef449a3ef8e672b80c9746 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 10 Dec 2024 23:13:15 -0500 Subject: [PATCH 95/98] CI: update upload-artifact, download-artifact, gh-action-pypi-publish --- .github/workflows/release.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 88954a9..f8b4f83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: run: python -m build - name: Save built packages as artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: packages-${{ runner.os }}-${{ steps.setup.outputs.python-version }} path: dist/ @@ -46,12 +46,14 @@ jobs: id-token: write steps: - name: Download packages - uses: actions/download-artifact@v3 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + pattern: packages-* + path: dist + merge-multiple: true - - name: Consolidate packages for upload - run: | - mkdir dist - cp packages-*/* dist/ + - name: Print out packages + run: ls dist - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@b7f401de30cb6434a1e19f805ff006643653240e # v1.8.10 + uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2 From 2bfda1cfdead91e79deea2644edea41f5ea3d461 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 10 Dec 2024 23:14:21 -0500 Subject: [PATCH 96/98] CI: add attestation --- .github/workflows/release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8b4f83..293337a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,5 +55,10 @@ jobs: - name: Print out packages run: ls dist + - name: Generate artifact attestation for sdist and wheel + uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + with: + subject-path: dist/cycler-* + - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2 From 6e74aa9ddb7e3c4237ba7af7814afd706a2e7050 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 16 Dec 2024 13:16:18 -0500 Subject: [PATCH 97/98] CI: update again --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 293337a..024c8d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,9 +56,9 @@ jobs: run: ls dist - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 + uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0 with: subject-path: dist/cycler-* - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2 + uses: pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3 From 4638537c23681aaab31b989393afa20192d5da98 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 27 Feb 2025 11:26:54 -0500 Subject: [PATCH 98/98] FIX: check that lengths match with in-place addition --- cycler/__init__.py | 4 ++++ test_cycler.py | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cycler/__init__.py b/cycler/__init__.py index 1f05670..db476b8 100644 --- a/cycler/__init__.py +++ b/cycler/__init__.py @@ -355,6 +355,10 @@ def __iadd__(self, other: Cycler[K, V]) -> Cycler[K, V]: # type: ignore[misc] """ if not isinstance(other, Cycler): raise TypeError("Cannot += with a non-Cycler object") + if len(self) != len(other): + raise ValueError( + f"Can only add equal length cycles, not {len(self)} and {len(other)}" + ) # True shallow copy of self is fine since this is in-place old_self = copy.copy(self) self._keys = _process_keys(old_self, other) diff --git a/test_cycler.py b/test_cycler.py index 637521b..1eccac3 100644 --- a/test_cycler.py +++ b/test_cycler.py @@ -65,12 +65,22 @@ def test_prod(): _cycler_helper(c3 * c1, 45, ['lw', 'c'], target) -def test_inplace(): +def test_inplace_add(): c1 = cycler(c='rgb') c2 = cycler(lw=range(3)) c2 += c1 _cycler_helper(c2, 3, ['c', 'lw'], [list('rgb'), range(3)]) + +def test_inplace_add_len_mismatch(): + # miss-matched add lengths + c1 = cycler(c='rgb') + c3 = cycler(lw=range(15)) + with pytest.raises(ValueError): + c1 += c3 + + +def test_inplace_mul(): c3 = cycler(c='rgb') c4 = cycler(lw=range(3)) c3 *= c4