From 785e66cf65eca9c4076d020fb0e6f605fc1b62c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Thu, 20 Jul 2023 09:29:47 +0200 Subject: [PATCH] Add repo setup (#1) Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> --- .github/CODEOWNERS | 7 + .github/ISSUE_TEMPLATE/BUG_REPORT.yaml | 50 ++++ .github/ISSUE_TEMPLATE/FEATURE_REQUEST.yaml | 14 + .github/PULL_REQUEST_TEMPLATE.md | 18 ++ .github/actions/install-algorithms/action.yml | 22 ++ .../install-main-dependencies/action.yml | 78 ++++++ .github/actions/run-tests/action.yml | 43 ++++ .github/workflows/deploy-code.yml | 39 +++ .github/workflows/main.yml | 157 ++++++++++++ .mergify.yml | 23 ++ .pylintdict | 0 .stestr.conf | 2 + MANIFEST.in | 7 + Makefile | 72 ++++++ pyproject.toml | 76 ++++++ releasenotes/config.yaml | 3 + requirements-dev.txt | 10 +- tools/check_copyright.py | 242 ++++++++++++++++++ tools/find_stray_release_notes.py | 58 +++++ tools/ignore_untagged_notes.sh | 38 +++ tools/rclone.conf.enc | Bin 0 -> 304 bytes tools/verify_headers.py | 110 ++++++++ tox.ini | 45 ++++ 23 files changed, 1106 insertions(+), 8 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/BUG_REPORT.yaml create mode 100644 .github/ISSUE_TEMPLATE/FEATURE_REQUEST.yaml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/actions/install-algorithms/action.yml create mode 100644 .github/actions/install-main-dependencies/action.yml create mode 100644 .github/actions/run-tests/action.yml create mode 100644 .github/workflows/deploy-code.yml create mode 100644 .github/workflows/main.yml create mode 100644 .mergify.yml create mode 100644 .pylintdict create mode 100644 .stestr.conf create mode 100644 MANIFEST.in create mode 100644 Makefile create mode 100644 pyproject.toml create mode 100644 releasenotes/config.yaml create mode 100644 tools/check_copyright.py create mode 100755 tools/find_stray_release_notes.py create mode 100755 tools/ignore_untagged_notes.sh create mode 100644 tools/rclone.conf.enc create mode 100755 tools/verify_headers.py create mode 100644 tox.ini diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..47278ecd --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# This file defines the persons who will be assigned as reviewers for PRs that +# modify particular files in the repo. The PR can be merged when approved by at +# least one codeowner. However, all Qiskit team members can (and should!) review the PRs. + +# Global rule, unless specialized by a later one +* @Cryoris @woodsp-ibm @ElePT + diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml new file mode 100644 index 00000000..7102210f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yaml @@ -0,0 +1,50 @@ +name: 🐛 Bug report +description: Create a report to help us improve 🤔. +labels: ["bug"] + +body: + - type: markdown + attributes: + value: Thank you for reporting! Please also use the search to see if there are any other relevant issues or pull requests. + + - type: textarea + attributes: + label: Environment + description: For the version of Algorithms, please give the actual version number (_e.g._ 0.18.3) if you are using a release version, or the first 7-8 characters of the commit hash if you have installed from `git`. If anything else is relevant, you can add it to the list. + # The trailing spaces on the following lines are to make filling the form + # in easier. The type is 'textarea' rather than three separate 'input's + # to make the resulting issue body less noisy with headings. + value: | + - **Qiskit Algorithms version**: + - **Python version**: + - **Operating system**: + validations: + required: true + + - type: textarea + attributes: + label: What is happening? + description: A short description of what is going wrong, in words. + validations: + required: true + + - type: textarea + attributes: + label: How can we reproduce the issue? + description: Give some steps that show the bug. A [minimal working example](https://stackoverflow.com/help/minimal-reproducible-example) of code with output is best. If you are copying in code, please remember to enclose it in triple backticks (` ``` [multiline code goes here] ``` `) so that it [displays correctly](https://docs.github.com/en/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). + validations: + required: true + + - type: textarea + attributes: + label: What should happen? + description: A short description, including about the expected output of any code in the previous section. + validations: + required: true + + - type: textarea + attributes: + label: Any suggestions? + description: Not required, but if you have suggestions for how a contributor should fix this, or any problems we should be aware of, let us know. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yaml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yaml new file mode 100644 index 00000000..4b66a76c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yaml @@ -0,0 +1,14 @@ +name: 🚀 Feature request +description: Suggest an idea for this project 💡! +labels: ["type: feature request"] + +body: + - type: markdown + attributes: + value: Please make sure to browse the opened and closed issues to make sure that this idea has not previously been discussed. + + - type: textarea + attributes: + label: What should we add? + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..38511656 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ + + +### Summary + + + +### Details and comments + + diff --git a/.github/actions/install-algorithms/action.yml b/.github/actions/install-algorithms/action.yml new file mode 100644 index 00000000..23caf77a --- /dev/null +++ b/.github/actions/install-algorithms/action.yml @@ -0,0 +1,22 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +name: 'Install Qiskit Algorithms' +description: 'Installs Qiskit Algorithms' + +runs: + using: "composite" + steps: + - run : | + pip install -e . + pip install -U -c constraints.txt -r requirements-dev.txt + shell: bash diff --git a/.github/actions/install-main-dependencies/action.yml b/.github/actions/install-main-dependencies/action.yml new file mode 100644 index 00000000..23663700 --- /dev/null +++ b/.github/actions/install-main-dependencies/action.yml @@ -0,0 +1,78 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +name: 'Install Qiskit Algorithms Main Dependencies' +description: 'Installs Python dependencies from Main' +inputs: + os: + description: 'OS' + required: true + python-version: + description: 'Python version' + required: true + terra-main: + description: 'Use Terra main' + required: true +runs: + using: "composite" + steps: + - name: Get main last commit ids + run: | + echo "TERRA_HASH=$(git ls-remote --heads https://github.com/Qiskit/qiskit-terra.git refs/heads/main | awk '{print $1}')" >> $GITHUB_ENV + shell: bash + - name: Terra Cache + env: + CACHE_VERSION: v1 + id: terra-cache + uses: actions/cache@v3 + with: + path: terra-cache + key: terra-cache-${{ inputs.os }}-${{ inputs.python-version }}-${{ env.TERRA_HASH }}-${{ env.CACHE_VERSION }} + - name: Install Terra from Main + env: + MACOSX_DEPLOYMENT_TARGET: 10.15 + run: | + if [ "${{ inputs.terra-main }}" == "true" ]; then + echo 'Install Terra from Main' + BASE_DIR=terra-cache + build_from_main=true + cache_hit=${{ steps.terra-cache.outputs.cache-hit }} + echo "cache hit: ${cache_hit}" + if [ "$cache_hit" == "true" ]; then + pip_result=0 + pushd "${BASE_DIR}" + python -m pip install *.whl && pip_result=$? || pip_result=$? + popd + if [ $pip_result == 0 ]; then + build_from_main=false + fi + else + mkdir -p ${BASE_DIR} + fi + if [ "$build_from_main" == "true" ]; then + echo 'Create wheel file from main' + pip install -U wheel setuptools_rust + git clone --depth 1 --branch main https://github.com/Qiskit/qiskit-terra.git /tmp/qiskit-terra + pushd /tmp/qiskit-terra + python setup.py bdist_wheel + popd + cp -rf /tmp/qiskit-terra/dist/*.whl "${BASE_DIR}" + pushd "${BASE_DIR}" + python -m pip install *.whl + popd + pip uninstall -y setuptools_rust + fi + else + echo 'Install Terra from Stable' + pip install -U qiskit-terra + fi + shell: bash diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml new file mode 100644 index 00000000..107700ba --- /dev/null +++ b/.github/actions/run-tests/action.yml @@ -0,0 +1,43 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +name: 'Run Unit Tests' +description: 'Run Unit Tests' +inputs: + os: + description: 'OS' + required: true + event-name: + description: 'Actions event' + required: true + run-slow: + description: 'Run slow tests or not' + required: true + python-version: + description: 'Python version' + required: true +runs: + using: "composite" + steps: + - name: Run Unit Tests + env: + PYTHONWARNINGS: default + run: | + # run slow tests only on scheduled event or if input flag is set + if [ "${{ inputs.event-name }}" == "schedule" ] || [ "${{ inputs.run-slow }}" == "true" ]; then + export QISKIT_TESTS="run_slow" + fi + if [ "${{ inputs.os }}" == "ubuntu-latest" ] && [ "${{ inputs.python-version }}" == "3.8" ]; then + export PYTHON="coverage3 run --source qiskit_algorithms --parallel-mode" + fi + stestr --test-path test run 2> >(tee /dev/stderr out.txt > /dev/null) + shell: bash diff --git a/.github/workflows/deploy-code.yml b/.github/workflows/deploy-code.yml new file mode 100644 index 00000000..3256eb69 --- /dev/null +++ b/.github/workflows/deploy-code.yml @@ -0,0 +1,39 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +name: Deploy Code + +on: + push: + tags: + - '*' + +jobs: + code_publish: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Deploy to Pypi + env: + TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + TWINE_USERNAME: qiskit + run : | + pip install -U twine pip setuptools virtualenv wheel + python3 setup.py sdist bdist_wheel + twine upload dist/qiskit* + shell: bash diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..a1b26578 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,157 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +name: Algorithms Unit Tests + +on: + push: + branches: + - main + - 'stable/**' + pull_request: + branches: + - main + - 'stable/**' + schedule: + # run every day at 1AM + - cron: '0 1 * * *' + +concurrency: + group: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + Checks: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.8] + steps: + - name: Print Concurrency Group + env: + CONCURRENCY_GROUP: ${{ github.repository }}-${{ github.ref }}-${{ github.head_ref }}-${{ github.workflow }} + run: | + echo -e "\033[31;1;4mConcurrency Group\033[0m" + echo -e "$CONCURRENCY_GROUP\n" + shell: bash + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: | + setup.py + requirements.txt + requirements-dev.txt + - uses: ./.github/actions/install-main-dependencies + with: + os: ${{ matrix.os }} + python-version: ${{ matrix.python-version }} + terra-main: "false" + if: ${{ !startsWith(github.ref, 'refs/heads/stable') && !startsWith(github.base_ref, 'stable/') }} + - uses: ./.github/actions/install-algorithms + - name: Install Dependencies + run: | + sudo apt-get -y install pandoc graphviz python3-enchant hunspell-en-us + pip install pyenchant + echo "earliest_version: 0.1.0" >> releasenotes/config.yaml + shell: bash + - run: pip check + if: ${{ !cancelled() }} + shell: bash + - name: Style Check + run: | + make style + if: ${{ !cancelled() }} + shell: bash + Algorithms: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: [3.8, 3.9, '3.10', 3.11] + include: + - os: macos-latest + python-version: 3.8 + - os: macos-latest + python-version: 3.11 + - os: windows-2019 + python-version: 3.8 + - os: windows-2019 + python-version: 3.11 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: | + setup.py + requirements.txt + requirements-dev.txt + - uses: ./.github/actions/install-main-dependencies + with: + os: ${{ matrix.os }} + python-version: ${{ matrix.python-version }} + if: ${{ !startsWith(github.ref, 'refs/heads/stable') && !startsWith(github.base_ref, 'stable/') }} + - uses: ./.github/actions/install-algorithms + - run: make lint + shell: bash + - name: Algorithms Unit Tests under Python ${{ matrix.python-version }} + uses: ./.github/actions/run-tests + with: + os: ${{ matrix.os }} + event-name: ${{ github.event_name }} + run-slow: ${{ contains(github.event.pull_request.labels.*.name, 'run_slow') }} + python-version: ${{ matrix.python-version }} + if: ${{ !cancelled() }} + - name: Coverage combine + run: | + coverage3 combine + mkdir ./ci-artifact-data + mv .coverage ./ci-artifact-data/alg.dat + if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == 3.8 }} + shell: bash + - uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.os }}-${{ matrix.python-version }} + path: ./ci-artifact-data/* + Deprecation_Messages_and_Coverage: + needs: [Checks, Algorithms] + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/download-artifact@v3 + with: + name: ubuntu-latest-3.8 + path: /tmp/u38 + - name: Install Dependencies + run: pip install -U coverage coveralls diff-cover + shell: bash + - name: Coverage combine + run: coverage3 combine /tmp/u38/alg.dat + shell: bash + - name: Upload to Coveralls + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: coveralls --service=github + shell: bash diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 00000000..70226b82 --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,23 @@ +queue_rules: + - name: automerge + conditions: + - check-success=Deprecation_Messages_and_Coverage (3.8) + +pull_request_rules: + - name: automatic merge on CI success and review + conditions: + - check-success=Deprecation_Messages_and_Coverage (3.8) + - "#approved-reviews-by>=1" + - label=automerge + - label!=on hold + actions: + queue: + name: automerge + method: squash + - name: backport + conditions: + - label=stable backport potential + actions: + backport: + branches: + - stable/0.1 diff --git a/.pylintdict b/.pylintdict new file mode 100644 index 00000000..e69de29b diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 00000000..fa12ebad --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,2 @@ +[DEFAULT] +test_path=./test \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..284314c6 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include LICENSE.txt +include requirements.txt +include qiskit_algorithms/py.typed +recursive-include qiskit_algorithms *.txt +graft test +prune tools +global-exclude *.py[co] .DS_Store diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..d60fd19a --- /dev/null +++ b/Makefile @@ -0,0 +1,72 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +OS := $(shell uname -s) + +ifeq ($(OS), Linux) + NPROCS := $(shell grep -c ^processor /proc/cpuinfo) +else ifeq ($(OS), Darwin) + NPROCS := 2 +else + NPROCS := 0 +endif # $(OS) + +ifeq ($(NPROCS), 2) + CONCURRENCY := 2 +else ifeq ($(NPROCS), 1) + CONCURRENCY := 1 +else ifeq ($(NPROCS), 3) + CONCURRENCY := 3 +else ifeq ($(NPROCS), 0) + CONCURRENCY := 0 +else + CONCURRENCY := $(shell echo "$(NPROCS) 2" | awk '{printf "%.0f", $$1 / $$2}') +endif + +# You can set this variable from the command line. +SPHINXOPTS = + +.PHONY: lint style black test test_ci spell copyright coverage clean + +all_check: spell style lint copyright + +lint: + pylint -rn qiskit_algorithms test tools + python tools/verify_headers.py qiskit_algorithms test tools + +style: + black --check qiskit_algorithms test tools + +black: + black qiskit_algorithms test tools + +test: + python -m unittest discover -v test + +test_ci: + echo "Detected $(NPROCS) CPUs running with $(CONCURRENCY) workers" + stestr run --concurrency $(CONCURRENCY) + +spell: + pylint -rn --disable=all --enable=spelling --spelling-dict=en_US --spelling-private-dict-file=.pylintdict qiskit_algorithms test tools + +copyright: + python tools/check_copyright.py + +coverage: + coverage3 run --source qiskit_algorithms -m unittest discover -s test -q + coverage3 report + +coverage_erase: + coverage erase + +clean: coverage_erase; diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..d1ffad6c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,76 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 100 +target-version = ['py38', 'py39', 'py310', 'py311'] + +[tool.pylint.main] +extension-pkg-allow-list = [ + "numpy", + "rustworkx", +] +load-plugins = ["pylint.extensions.docparams", "pylint.extensions.docstyle"] +py-version = "3.8" # update it when bumping minimum supported python version + +[tool.pylint.basic] +good-names = ["a", "b", "i", "j", "k", "d", "n", "m", "ex", "v", "w", "x", "y", "z", "Run", "_", "logger", "q", "c", "r", "qr", "cr", "qc", "nd", "pi", "op", "b", "ar", "br", "p", "cp", "ax", "dt", "__unittest", "iSwapGate", "mu"] +method-rgx = "(([a-z_][a-z0-9_]{2,49})|(assert[A-Z][a-zA-Z0-9]{2,43})|(test_[_a-zA-Z0-9]{2,}))$" +variable-rgx = "[a-z_][a-z0-9_]{1,30}$" + +[tool.pylint.format] +max-line-length = 105 # default 100 + +[tool.pylint."messages control"] +disable = [ +# intentionally disabled: + "spelling", # too noisy + "fixme", # disabled as TODOs would show up as warnings + "protected-access", # disabled as we don't follow the public vs private convention strictly + "duplicate-code", # disabled as it is too verbose + "redundant-returns-doc", # for @abstractmethod, it cannot interpret "pass" + "too-many-lines", "too-many-branches", "too-many-locals", "too-many-nested-blocks", "too-many-statements", + "too-many-instance-attributes", "too-many-arguments", "too-many-public-methods", "too-few-public-methods", "too-many-ancestors", + "unnecessary-pass", # allow for methods with just "pass", for clarity + "no-else-return", # relax "elif" after a clause with a return + "docstring-first-line-empty", # relax docstring style + "import-outside-toplevel", "import-error", # overzealous with our optionals/dynamic packages +# TODO(#9614): these were added in modern Pylint. Decide if we want to enable them. If so, +# remove from here and fix the issues. Else, move it above this section and add a comment +# with the rationale + "arguments-renamed", + "broad-exception-raised", + "consider-iterating-dictionary", + "consider-using-dict-items", + "consider-using-enumerate", + "consider-using-f-string", + "modified-iterating-list", + "nested-min-max", + "no-member", + "no-value-for-parameter", + "non-ascii-name", + "not-context-manager", + "superfluous-parens", + "unknown-option-value", + "unexpected-keyword-arg", + "unnecessary-dict-index-lookup", + "unnecessary-direct-lambda-call", + "unnecessary-dunder-call", + "unnecessary-ellipsis", + "unnecessary-lambda-assignment", + "unnecessary-list-index-lookup", + "unspecified-encoding", + "unsupported-assignment-operation", + "use-dict-literal", + "use-list-literal", + "use-implicit-booleaness-not-comparison", + "use-maxsplit-arg", +] + +enable = [ + "use-symbolic-message-instead" +] + +[tool.pylint.spelling] +spelling-private-dict-file = ".local-spellings" diff --git a/releasenotes/config.yaml b/releasenotes/config.yaml new file mode 100644 index 00000000..141d552c --- /dev/null +++ b/releasenotes/config.yaml @@ -0,0 +1,3 @@ +--- +encoding: utf8 +default_branch: main diff --git a/requirements-dev.txt b/requirements-dev.txt index facf0d74..9271ef3a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,15 +1,9 @@ coverage>=4.4.0,<7.0 matplotlib>=3.3 -black[jupyter]~=22.0 +black~=22.0 pylint>=2.15.0 -pylatexenc>=1.4 stestr>=2.0.0 ddt>=1.2.0,!=1.4.0 reno>=3.4.0 -Sphinx>=5.0 -sphinx-design>=0.4.0 -sphinxcontrib-spelling -jupyter-sphinx discover -nbsphinx -qiskit_sphinx_theme~=1.12.0 +rustworkx>=0.13 diff --git a/tools/check_copyright.py b/tools/check_copyright.py new file mode 100644 index 00000000..003437e3 --- /dev/null +++ b/tools/check_copyright.py @@ -0,0 +1,242 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" Fix copyright year in header """ + +from typing import Tuple, Union, List +import builtins +import sys +import os +import datetime +import argparse +import subprocess +import traceback + + +class CopyrightChecker: + """Check copyright""" + + _UTF_STRING = "# -*- coding: utf-8 -*-" + _COPYRIGHT_STRING = "# (C) Copyright IBM " + + def __init__(self, root_dir: str, check: bool) -> None: + self._root_dir = root_dir + self._check = check + self._current_year = datetime.datetime.now().year + self._changed_files = self._get_changed_files() + + @staticmethod + def _exception_to_string(excp: Exception) -> str: + stack = traceback.extract_stack()[:-3] + traceback.extract_tb(excp.__traceback__) + pretty = traceback.format_list(stack) + return "".join(pretty) + f"\n {excp.__class__} {excp}" + + @staticmethod + def _get_year_from_date(date) -> int: + if not date or len(date) < 4: + return None + + return int(date[:4]) + + def _cmd_execute(self, args: List[str]) -> Tuple[str, Union[None, str]]: + # execute command + env = {} + for k in ["SYSTEMROOT", "PATH"]: + v = os.environ.get(k) + if v is not None: + env[k] = v + # LANGUAGE is used on win32 + env["LANGUAGE"] = "C" + env["LANG"] = "C" + env["LC_ALL"] = "C" + with subprocess.Popen( + args, + cwd=self._root_dir, + env=env, + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) as popen: + out, err = popen.communicate() + popen.wait() + out_str = out.decode("utf-8").strip() + err_str = err.decode("utf-8").strip() + err_str = err_str if err_str else None + return out_str, err_str + + def _get_changed_files(self) -> List[str]: + out_str, err_str = self._cmd_execute(["git", "diff", "--name-only", "HEAD"]) + if err_str: + raise builtins.Exception(err_str) + + return out_str.splitlines() + + def _get_file_last_year(self, relative_path: str) -> int: + last_year = None + errors = [] + try: + out_str, err_str = self._cmd_execute( + ["git", "log", "-1", "--format=%cI", relative_path] + ) + last_year = CopyrightChecker._get_year_from_date(out_str) + if err_str: + errors.append(err_str) + except builtins.Exception as ex: # pylint: disable=broad-except + errors.append(f"'{relative_path}' Last year: {str(ex)}") + + if errors: + raise ValueError(" - ".join(errors)) + + return last_year + + def check_copyright(self, file_path) -> Tuple[bool, bool, bool]: + """check copyright for a file""" + file_with_utf8 = False + file_with_invalid_year = False + file_has_header = False + try: + new_line = "# (C) Copyright IBM " + idx_utf8 = -1 + idx_new_line = -1 + file_lines = None + with open(file_path, "rt", encoding="utf8") as file: + file_lines = file.readlines() + for idx, line in enumerate(file_lines): + relative_path = os.path.relpath(file_path, self._root_dir) + if line.startswith(CopyrightChecker._UTF_STRING): + if self._check: + print(f"File contains utf-8 header: '{relative_path}'") + file_with_utf8 = True + idx_utf8 = idx + + if not line.startswith(CopyrightChecker._COPYRIGHT_STRING): + continue + + file_has_header = True + curr_years = [] + for word in line.strip().split(): + for year in word.strip().split(","): + if year.startswith("20") and len(year) >= 4: + try: + curr_years.append(int(year[0:4])) + except ValueError: + pass + + header_start_year = None + header_last_year = None + if len(curr_years) > 1: + header_start_year = curr_years[0] + header_last_year = curr_years[1] + elif len(curr_years) == 1: + header_start_year = header_last_year = curr_years[0] + + if relative_path in self._changed_files: + self._changed_files.remove(relative_path) + last_year = self._current_year + else: + last_year = self._get_file_last_year(relative_path) + if last_year and header_last_year != last_year: + if header_start_year and header_start_year != last_year: + new_line += f"{header_start_year}, " + + new_line += f"{self._current_year}.\n" + if self._check: + print( + f"Wrong Copyright Year:'{relative_path}': ", + f"Current:'{line[:-1]}' Correct:'{new_line[:-1]}'", + ) + file_with_invalid_year = True + idx_new_line = idx + + break + if not self._check and (idx_utf8 >= 0 or idx_new_line >= 0): + if idx_new_line >= 0: + file_lines[idx_new_line] = new_line + if idx_utf8 >= 0: + del file_lines[idx_utf8] + with open(file_path, "w", encoding="utf8") as file: + file.writelines(file_lines) + if idx_new_line >= 0: + file_with_invalid_year = False + print(f"Fixed copyright year for {relative_path}.") + if idx_utf8 >= 0: + file_with_utf8 = False + print(f"Removed utf-8 header for {relative_path}.") + + except UnicodeDecodeError: + return file_with_utf8, file_with_invalid_year, file_has_header + + return file_with_utf8, file_with_invalid_year, file_has_header + + def check(self) -> Tuple[int, int, int]: + """check copyright""" + return self._check_copyright(self._root_dir) + + def _check_copyright(self, path: str) -> Tuple[int, int, int]: + files_with_utf8 = 0 + files_with_invalid_year = 0 + files_with_header = 0 + for item in os.listdir(path): + fullpath = os.path.join(path, item) + if os.path.isdir(fullpath): + if not item.startswith("."): + files = self._check_copyright(fullpath) + files_with_utf8 += files[0] + files_with_invalid_year += files[1] + files_with_header += files[2] + continue + + if os.path.isfile(fullpath): + # check copyright year + ( + file_with_utf8, + file_with_invalid_year, + file_has_header, + ) = self.check_copyright(fullpath) + if file_with_utf8: + files_with_utf8 += 1 + if file_with_invalid_year: + files_with_invalid_year += 1 + if file_has_header: + files_with_header += 1 + + return files_with_utf8, files_with_invalid_year, files_with_header + + +def check_path(path): + """valid path argument""" + if not path or os.path.isdir(path): + return path + + raise argparse.ArgumentTypeError(f"readable_dir:{path} is not a valid path") + + +if __name__ == "__main__": + PARSER = argparse.ArgumentParser(description="Check Copyright Tool") + PARSER.add_argument("-path", type=check_path, metavar="path", help="Root path of project.") + PARSER.add_argument( + "-check", + required=False, + action="store_true", + help="Just check copyright, without fixing it.", + ) + + ARGS = PARSER.parse_args() + if not ARGS.path: + ARGS.path = os.getcwd() + + ARGS.path = os.path.abspath(os.path.realpath(os.path.expanduser(ARGS.path))) + INVALID_UTF8, INVALID_YEAR, HAS_HEADER = CopyrightChecker(ARGS.path, ARGS.check).check() + print(f"{INVALID_UTF8} files have utf8 headers.") + print(f"{INVALID_YEAR} of {HAS_HEADER} files with copyright header have wrong years.") + + sys.exit(0 if INVALID_UTF8 == 0 and INVALID_YEAR == 0 else 1) diff --git a/tools/find_stray_release_notes.py b/tools/find_stray_release_notes.py new file mode 100755 index 00000000..d694e0d8 --- /dev/null +++ b/tools/find_stray_release_notes.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022 +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Utility script to verify qiskit copyright file headers""" + +import argparse +import multiprocessing +import subprocess +import sys +import re + +# release notes regex +reno = re.compile(r"releasenotes\/notes") +# exact release note regex +exact_reno = re.compile(r"^releasenotes\/notes") + + +def discover_files(): + """Find all .py, .pyx, .pxd files in a list of trees""" + cmd = ["git", "ls-tree", "-r", "--name-only", "HEAD"] + res = subprocess.run(cmd, capture_output=True, check=True, encoding="UTF8") + files = res.stdout.split("\n") + return files + + +def validate_path(file_path): + """Validate a path in the git tree.""" + if reno.search(file_path) and not exact_reno.search(file_path): + return file_path + return None + + +def _main(): + parser = argparse.ArgumentParser(description="Find any stray release notes.") + _args = parser.parse_args() + files = discover_files() + with multiprocessing.Pool() as pool: + res = pool.map(validate_path, files) + failed_files = [x for x in res if x is not None] + if len(failed_files) > 0: + for failed_file in failed_files: + sys.stderr.write(f"{failed_file} is not in the correct location.\n") + sys.exit(1) + sys.exit(0) + + +if __name__ == "__main__": + _main() diff --git a/tools/ignore_untagged_notes.sh b/tools/ignore_untagged_notes.sh new file mode 100755 index 00000000..b31071b0 --- /dev/null +++ b/tools/ignore_untagged_notes.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021, 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# Script to ignore untagged release notes prior to deploying docs to site. + +LATEST_TAG=$(git describe --tags --abbrev=0) + +# select only release notes added after the tag was created +for file_changed in `git diff --name-only HEAD $LATEST_TAG releasenotes/notes` +do + if [[ $file_changed == releasenotes/notes/* ]]; then + isInFile=$(grep -Exq "\s*$file_changed," docs/release_notes.rst >/dev/null; echo $?) + if [ $isInFile -ne 0 ]; then + isInFile=$(grep -Exq "\s*:ignore-notes:\s*" docs/release_notes.rst >/dev/null; echo $?) + if [ $isInFile -ne 0 ]; then + echo " :ignore-notes:" >> docs/release_notes.rst + fi + echo "Release note changed since $LATEST_TAG: $file_changed. Ignore in docs/release_notes.rst" + echo " $file_changed," >> docs/release_notes.rst + fi + fi +done + +echo "Contents of docs/release_notes.rst:" +echo "$(cat docs/release_notes.rst)" + +exit 0 diff --git a/tools/rclone.conf.enc b/tools/rclone.conf.enc new file mode 100644 index 0000000000000000000000000000000000000000..985bd728abc0a83d8ea98cd4d9561b7fa124842f GIT binary patch literal 304 zcmV-00nh$7&RTYTNLa46ND6UrOuMoPNp}L^N21;+KWICI2ddxLf?x*g*GAzexAhvW z5rTO-?xi4$c>vaY~!DfD~lI0H5)o5;H>qj7M~)ZT{14Fvc91%J)Ycl~B`S zR;dTAK}Qz7!C#ExhwZKgVKh_&DPch2pvl7`Df`TB7^fDm2w+?}@Ltb_s9A^-JfyD- zcV@+wP8bfhSO=k!OfNS+tVO*B2xkEIky>2YRz;z0Ar#-=dP|4$ar~If5$=F}D=bc3 C!HCcR literal 0 HcmV?d00001 diff --git a/tools/verify_headers.py b/tools/verify_headers.py new file mode 100755 index 00000000..edaca651 --- /dev/null +++ b/tools/verify_headers.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020, 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Utility script to verify qiskit copyright file headers""" + +import argparse +import multiprocessing +import os +import sys +import re + +# regex for character encoding from PEP 263 +pep263 = re.compile(r"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") + + +def discover_files(code_paths, exclude_folders): + """Find all .py, .pyx, .pxd files in a list of trees""" + out_paths = [] + for path in code_paths: + if os.path.isfile(path): + out_paths.append(path) + else: + for directory in os.walk(path): + dir_path = directory[0] + for folder in exclude_folders: + if folder in directory[1]: + directory[1].remove(folder) + for subfile in directory[2]: + if ( + subfile.endswith(".py") + or subfile.endswith(".pyx") + or subfile.endswith(".pxd") + ): + out_paths.append(os.path.join(dir_path, subfile)) + return out_paths + + +def validate_header(file_path): + """Validate the header for a single file""" + header = """# This code is part of Qiskit. +# +""" + apache_text = """# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +""" + count = 0 + with open(file_path, encoding="utf8") as code_file: + lines = code_file.readlines() + start = 0 + for index, line in enumerate(lines): + count += 1 + if count > 5: + return file_path, False, "Header not found in first 5 lines" + if count <= 2 and pep263.match(line): + return file_path, False, "Unnecessary encoding specification (PEP 263, 3120)" + if line == "# This code is part of Qiskit.\n": + start = index + break + if "".join(lines[start : start + 2]) != header: + return file_path, False, f"Header up to copyright line does not match: {header}" + if not lines[start + 2].startswith("# (C) Copyright IBM 20"): + return file_path, False, "Header copyright line not found" + if "".join(lines[start + 3 : start + 11]) != apache_text: + return file_path, False, f"Header apache text string doesn't match:\n {apache_text}" + return file_path, True, None + + +def _main(): + default_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "qiskit_algorithms" + ) + parser = argparse.ArgumentParser(description="Check file headers.") + parser.add_argument( + "paths", + type=str, + nargs="*", + default=[default_path], + help="Paths to scan by default uses ../qiskit_algorithms from the script", + ) + args = parser.parse_args() + files = discover_files(args.paths, exclude_folders=[]) + with multiprocessing.Pool() as pool: + res = pool.map(validate_header, files) + failed_files = [x for x in res if x[1] is False] + if len(failed_files) > 0: + for failed_file in failed_files: + sys.stderr.write(f"{failed_file[0]} failed header check because:\n") + sys.stderr.write(f"{failed_file[2]}\n\n") + sys.exit(1) + sys.exit(0) + + +if __name__ == "__main__": + _main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..2c2f6408 --- /dev/null +++ b/tox.ini @@ -0,0 +1,45 @@ +[tox] +# Sets this min.version because of differences with env_tmp_dir env. +minversion = 4.0.2 +envlist = py38, py39, py310, py311, lint +skipsdist = True + +[testenv] +usedevelop = True +install_command = pip install -c constraints.txt -U {opts} {packages} +passenv = * +setenv = + VIRTUAL_ENV={envdir} + LANGUAGE=en_US + LC_ALL=en_US.utf-8 + ARGS="-V" +deps = git+https://github.com/Qiskit/qiskit-terra.git + -r{toxinidir}/requirements.txt + -r{toxinidir}/requirements-dev.txt +commands = + stestr run {posargs} + +[testenv:lint] +envdir = .tox/lint +basepython = python3 +commands = + black --check {posargs} qiskit_algorithms test tools docs + pylint -rn qiskit_algorithms test tools + mypy qiskit_algorithms test tools + python3 {toxinidir}/tools/check_copyright.py -path {toxinidir} + python3 {toxinidir}/tools/verify_headers.py qiskit_algorithms test tools + python3 {toxinidir}/tools/find_stray_release_notes.py + +[testenv:black] +envdir = .tox/lint +commands = black {posargs} qiskit_algorithms test tools docs + +[testenv:coverage] +basepython = python3 +setenv = + {[testenv]setenv} + PYTHON=coverage3 run --source qiskit_algorithms --parallel-mode +commands = + stestr run {posargs} + coverage3 combine + coverage3 report