diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..6a5c910
--- /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 SPHINXOPTS="-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/.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/.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/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
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..024c8d9
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,64 @@
+---
+
+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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
+ 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@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ with:
+ pattern: packages-*
+ path: dist
+ merge-multiple: true
+
+ - name: Print out packages
+ run: ls dist
+
+ - name: Generate artifact attestation for sdist and wheel
+ 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@67339c736fd9354cd4f8cb0b744f2b82a74b5c70 # v1.12.3
diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml
new file mode 100644
index 0000000..ce95577
--- /dev/null
+++ b/.github/workflows/reviewdog.yml
@@ -0,0 +1,79 @@
+---
+name: Linting
+on:
+ push:
+ branches-ignore:
+ - auto-backport-of-pr-[0-9]+
+ - v[0-9]+.[0-9]+.[0-9x]+-doc
+ pull_request:
+
+jobs:
+ flake8:
+ name: flake8
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python 3
+ uses: actions/setup-python@v4
+ with:
+ python-version: 3.9
+
+ - name: Install flake8
+ run: pip3 install 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 | \
+ reviewdog -f=pep8 -name=flake8 \
+ -tee -reporter=github-check -filter-mode nofilter
+
+ mypy:
+ name: "Mypy"
+ runs-on: ubuntu-20.04
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: 3.9
+
+ - 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==1.5.1
+
+ - 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/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..98ceb87
--- /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-20.04
+
+ strategy:
+ matrix:
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ allow-prereleases: true
+
+ - name: Upgrade pip
+ run: |
+ python -m pip install --upgrade pip
+
+ - name: Install cycler
+ run: |
+ python -m pip install .[tests]
+
+ - name: Run pytest
+ run: |
+ pytest -raR -n auto --cov --cov-report=
+
+ - name: Upload code coverage
+ uses: codecov/codecov-action@v3
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
deleted file mode 100644
index c354030..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-language: python
-
-matrix:
- include:
- - python: 2.7
- - python: 3.4
- - python: 3.5
- - python: "nightly"
- env: PRE=--pre
- allow_failures:
- - python : "nightly"
-
-install:
- - python setup.py install
- - pip install coveralls six
-
-script:
- - python run_tests.py
-
-after_success:
- coveralls
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 815cfe5..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,4 +0,0 @@
-include run_tests.py
-include LICENSE
-recursive-include conda-recipe *
-recursive-include doc Makefile make.bat *.rst *.py
\ No newline at end of file
diff --git a/README.rst b/README.rst
index 2fc7323..18712c9 100644
--- a/README.rst
+++ b/README.rst
@@ -1,4 +1,21 @@
+|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| 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
+
cycler: composable cycles
=========================
-Docs: http://matplotlib.org/cycler/
+Docs: https://matplotlib.org/cycler/
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.
diff --git a/appveyor.yml b/appveyor.yml
index 2cddd81..57ca08c 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,42 +1,16 @@
# AppVeyor.com is a Continuous Integration service to build and run tests under
# 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: http://stackoverflow.com/a/13751649/163740
- CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci\\appveyor\\run_with_env.cmd"
+image: Visual Studio 2019
+environment:
matrix:
- - PYTHON: "C:\\Python27_32"
- PYTHON_VERSION: "2.7"
- PYTHON_ARCH: "32"
-
- - PYTHON: "C:\\Python27_64"
- PYTHON_VERSION: "2.7"
- PYTHON_ARCH: "64"
-
- - PYTHON: "C:\\Python34_32"
- PYTHON_VERSION: "3.4.3"
- PYTHON_ARCH: "32"
-
- - PYTHON: "C:\\Python34_64"
- PYTHON_VERSION: "3.4.3"
- PYTHON_ARCH: "64"
-
- - PYTHON: "C:\\Python35"
- PYTHON_VERSION: "3.5.0"
- PYTHON_ARCH: "32"
-
- - PYTHON: "C:\\Python35-x64"
- PYTHON_VERSION: "3.5.0"
- PYTHON_ARCH: "64"
+ - PYTHON: "C:\\Python38"
+ - PYTHON: "C:\\Python38-x64"
+ - PYTHON: "C:\\Python39"
+ - PYTHON: "C:\\Python39-x64"
install:
- # Install Python (from the official .msi of http://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
@@ -44,7 +18,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"
+ - "pip install -v pytest pytest-cov pytest-xdist"
# Install the generated wheel package to test it
- "python setup.py install"
@@ -55,8 +29,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/ci/appveyor/install.ps1 b/ci/appveyor/install.ps1
deleted file mode 100644
index 0f165d8..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: http://creativecommons.org/publicdomain/zero/1.0/
-
-$MINICONDA_URL = "http://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
diff --git a/ci/appveyor/run_with_env.cmd b/ci/appveyor/run_with_env.cmd
deleted file mode 100644
index 0c70d63..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
-:: http://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
-)
diff --git a/conda-recipe/bld.bat b/conda-recipe/bld.bat
deleted file mode 100644
index 87b1481..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
-:: http://docs.continuum.io/conda/build.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 4d7fc03..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
-# http://docs.continuum.io/conda/build.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 ea33deb..0000000
--- a/conda-recipe/meta.yaml
+++ /dev/null
@@ -1,63 +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
- - six
-
- run:
- - python
- - six
-
-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: http://github.com/matplotlib/cycler
- license: BSD
- summary: 'Composable style cycles'
-
-# See
-# http://docs.continuum.io/conda/build.html for
-# more information about meta.yaml
diff --git a/cycler.py b/cycler/__init__.py
similarity index 58%
rename from cycler.py
rename to cycler/__init__.py
index 3c3eb2d..db476b8 100644
--- a/cycler.py
+++ b/cycler/__init__.py
@@ -40,43 +40,89 @@
{'color': 'b', 'linestyle': '-.'}
"""
-from __future__ import (absolute_import, division, print_function,
- unicode_literals)
-import six
+from __future__ import annotations
+
+from collections.abc import Hashable, Iterable, Generator
+import copy
+from functools import reduce
from itertools import product, cycle
-from six.moves import zip, reduce
from operator import mul, add
-import copy
+# Dict, List, Union required for runtime cast calls
+from typing import TypeVar, Generic, Callable, Union, Dict, List, Any, overload, cast
-__version__ = '0.10.0'
+__version__ = "0.13.0.dev0"
-def _process_keys(left, right):
+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]],
+ right: Cycler[K, V] | Iterable[dict[K, V]] | None,
+) -> set[K]:
"""
- 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 {}
- l_key = set(l_peek.keys())
- r_key = set(r_peek.keys())
- if l_key & r_key:
- raise ValueError("Can not compose overlapping cycles")
+ 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())
+ if common_keys := l_key & r_key:
+ raise ValueError(
+ f"Cannot compose overlapping cycles, duplicate key(s): {common_keys}"
+ )
+
return l_key | r_key
-class Cycler(object):
+def concat(left: Cycler[K, V], right: Cycler[K, U]) -> Cycler[K, V | U]:
+ 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'])
+
+ Returns
+ -------
+ `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 = 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))
+
+
+class Cycler(Generic[K, V]):
"""
- Composable cycles
+ Composable cycles.
This class has compositions methods:
@@ -87,62 +133,65 @@ class Cycler(object):
in-place ``+``
``*``
- for outer products (itertools.product) and integer multiplication
+ for outer products (`itertools.product`) and integer multiplication
``*=``
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
+ def __init__(
+ self,
+ left: Cycler[K, V] | Iterable[dict[K, V]] | None,
+ right: Cycler[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]] = 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
self._left = [copy.copy(v) for v in left]
else:
- self._left = None
+ self._left = []
if isinstance(right, Cycler):
- self._right = 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]
+ self._right: Cycler[K, V] | None = Cycler(
+ right._left, right._right, right._op
+ )
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):
- """
- The keys this Cycler knows about
- """
+ 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.
@@ -150,16 +199,17 @@ 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
if new in self._keys:
- raise ValueError("Can't replace %s with %s, %s is already a key" %
- (old, new, new))
+ raise ValueError(
+ f"Can't replace {old} with {new}, {new} is already a key"
+ )
if old not in self._keys:
- raise KeyError("Can't replace %s with %s, %s is not a key" %
- (old, new, old))
+ raise KeyError(
+ f"Can't replace {old} with {new}, {old} is not a key"
+ )
self._keys.remove(old)
self._keys.add(new)
@@ -177,19 +227,8 @@ 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
- with the proper operation (zip or product as of now)
- """
- 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):
+ 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
@@ -197,7 +236,7 @@ def _from_iter(cls, label, itr):
Parameters
----------
- label : str
+ label : hashable
The property key.
itr : iterable
@@ -205,84 +244,121 @@ def _from_iter(cls, label, itr):
Returns
-------
- cycler : Cycler
- New 'base' `Cycler`
+ `Cycler`
+ New 'base' cycler.
"""
- ret = cls(None)
+ ret: Cycler[K, V] = cls(None)
ret._left = list({label: v} for v in itr)
- ret._keys = set([label])
+ 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()
- 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__")
- def __iter__(self):
+ def __iter__(self) -> Generator[dict[K, V], None, None]:
if self._right is None:
- return iter(dict(l) for l in self._left)
-
- return self._compose()
-
- def __add__(self, other):
+ 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: Cycler[L, U]) -> Cycler[K | L, V | U]:
"""
- 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, "
- "not {0} and {1}".format(len(self), len(other)))
- return Cycler(self, other, zip)
+ raise ValueError(
+ f"Can only add equal length cycles, not {len(self)} and {len(other)}"
+ )
+ 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):
"""
- 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)
+ 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(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
+ @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):
- op_dict = {zip: min, product: mul}
+ def __len__(self) -> int:
+ 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)
- def __iadd__(self, other):
+ # 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 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")
+ 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)
@@ -291,14 +367,13 @@ 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]: # type: ignore[misc]
"""
- 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")
@@ -310,44 +385,45 @@ def __imul__(self, other):
self._right = Cycler(other._left, other._right, other._op)
return self
- def __eq__(self, other):
- """
- Check equality
- """
+ 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))
- def __repr__(self):
- op_map = {zip: '+', product: '*'}
+ __hash__ = None # type: ignore
+
+ def __repr__(self) -> str:
+ op_map = {zip: "+", product: "*"}
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, '?')
+ op = op_map.get(self._op, "?")
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)
for key in sorted_keys:
- output += "
{key!r}
".format(key=key)
+ output += f"
{key!r}
"
for d in iter(self):
output += "
"
for k in sorted_keys:
- output += "
{val!r}
".format(val=d[k])
+ output += f"
{d[k]!r}
"
output += "
"
output += "
"
return output
- def by_key(self):
- """Values by key
+ def by_key(self) -> dict[K, list[V]]:
+ """
+ Values by key.
This returns the transposed values of the cycler. Iterating
over a `Cycler` yields dicts with a single value for each key,
@@ -367,8 +443,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: dict[K, list[V]] = {k: list() for k in keys}
for d in self:
for k in keys:
@@ -378,91 +453,38 @@ def by_key(self):
# for back compatibility
_transpose = by_key
- def simplify(self):
- """Simplify the Cycler
-
- Returned as a composition using only sums (no multiplications)
+ def simplify(self) -> Cycler[K, V]:
+ """
+ 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 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.
+ concat = concat
- The keys must match exactly.
- This returns a single Cycler which is equivalent to
- `itertools.chain(self, other)`
+@overload
+def cycler(arg: Cycler[K, V]) -> Cycler[K, V]:
+ ...
- Examples
- --------
- >>> num = cycler('a', range(3))
- >>> let = cycler('a', 'abc')
- >>> num.concat(let)
- cycler('a', [0, 1, 2, 'a', 'b', 'c'])
+@overload
+def cycler(**kwargs: Iterable[V]) -> Cycler[str, V]:
+ ...
- 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))
+@overload
+def cycler(label: K, itr: Iterable[V]) -> Cycler[K, V]:
+ ...
def cycler(*args, **kwargs):
@@ -488,12 +510,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
@@ -506,36 +526,39 @@ def cycler(*args, **kwargs):
"""
if args and kwargs:
- raise TypeError("cyl() can only accept positional OR keyword "
- "arguments -- not both.")
+ raise TypeError(
+ "cycler() 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 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")
-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.
+ 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.
diff --git a/cycler/py.typed b/cycler/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/doc/source/conf.py b/doc/source/conf.py
index fb48a87..906a782 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.
@@ -13,13 +12,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 ------------------------------------------------
@@ -36,7 +32,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',
@@ -47,7 +42,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.
@@ -57,7 +52,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'
@@ -70,20 +65,20 @@
# |version| and |release|, also used in various other places throughout the
# built documents.
#
-# The short X.Y version.
-version = '0.10.0'
# The full version, including alpha/beta/rc tags.
-release = '0.10.0'
+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.
-#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.
@@ -94,24 +89,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 ----------------------------------------------
@@ -123,77 +118,77 @@
# 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,
# 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
# 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'
@@ -202,14 +197,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
@@ -222,23 +217,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 ---------------------------------------
@@ -251,7 +246,7 @@
]
# If true, show URL addresses after external links.
-#man_show_urls = False
+# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
@@ -266,19 +261,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': ('http://matplotlib.org', None)}
+intersphinx_mapping = {'python': ('https://docs.python.org/3', None),
+ 'matplotlb': ('https://matplotlib.org', None)}
-################# numpydoc config ####################
+# ################ numpydoc config ####################
numpydoc_show_class_members = False
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 911c403..126de4b 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -4,16 +4,16 @@
Composable cycles
===================
-.. htmlonly::
+.. only:: html
:Version: |version|
:Date: |today|
====== ====================================
-docs http://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
====== ====================================
@@ -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
@@ -39,21 +40,20 @@ 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
from __future__ import print_function
from cycler import cycler
-
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
@@ -155,7 +155,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
@@ -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.
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..c7c79cb
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,52 @@
+[project]
+name = "cycler"
+dynamic = ["version"]
+description = "Composable style cycles"
+authors = [
+ {name = "Thomas A Caswell", email = "matplotlib-users@python.org"},
+]
+readme = "README.rst"
+license = {file = "LICENSE"}
+requires-python = ">=3.8"
+classifiers = [
+ "License :: OSI Approved :: BSD License",
+ "Development Status :: 4 - Beta",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "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"]
+
+[project.urls]
+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"]
+
+[tool.setuptools.dynamic]
+version = {attr = "cycler.__version__"}
+
+[tool.setuptools.package-data]
+cycler = ["py.typed"]
+
+[build-system]
+requires = ["setuptools>=61"]
+build-backend = "setuptools.build_meta"
diff --git a/run_tests.py b/run_tests.py
deleted file mode 100644
index c47d8c7..0000000
--- a/run_tests.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/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)
-
-
-if __name__ == '__main__':
- run()
diff --git a/setup.py b/setup.py
deleted file mode 100644
index d70d29d..0000000
--- a/setup.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from setuptools import setup
-
-setup(name='cycler',
- version='0.10.0',
- author='Thomas A Caswell',
- author_email='matplotlib-users@python.org',
- py_modules=['cycler'],
- description='Composable style cycles',
- url='http://github.com/matplotlib/cycler',
- platforms='Cross platform (Linux, Mac OSX, Windows)',
- install_requires=['six'],
- 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',
- 'Programming Language :: Python :: 3.4',
- 'Programming Language :: Python :: 3.5',
- ],
- keywords='cycle kwargs',
- )
diff --git a/test_cycler.py b/test_cycler.py
index f65a4cd..1eccac3 100644
--- a/test_cycler.py
+++ b/test_cycler.py
@@ -1,131 +1,150 @@
-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
+
+import pytest # type: ignore
-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)
-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)
+ assert c1 == 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():
+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
- assert_raises(ValueError, add, c1, c3)
- assert_raises(ValueError, add, c3, c1)
+ 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():
+def test_inplace_add():
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)])
+
+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
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():
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():
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')
- 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():
@@ -135,20 +154,13 @@ 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():
c1 = cycler(lw=range(15))
- assert_raises(ValueError, Cycler.__getitem__, c1, 0)
- assert_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))
+ pytest.raises(ValueError, Cycler.__getitem__, c1, 0)
+ pytest.raises(ValueError, Cycler.__getitem__, c1, [0, 1])
def test_repr():
@@ -156,29 +168,43 @@ def test_repr():
# 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]))"
-
- yield _repr_tester_helper, '__repr__', c + c2, c_sum_rpr
- yield _repr_tester_helper, '__repr__', c * c2, c_prod_rpr
-
- sum_html = "