diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a509f50..843a090 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,8 +2,9 @@ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions # Based on ~/code/xcookie/xcookie/rc/tests.yml.in # Now based on ~/code/xcookie/xcookie/builders/github_actions.py +# See: https://github.com/Erotemic/xcookie -name: PurePy Build and Test +name: PurePyCI on: push: @@ -12,14 +13,19 @@ on: jobs: lint_job: + ## + # Run quick linting and typing checks. + # To disable all linting add "linter=false" to the xcookie config. + # To disable type checks add "notypes" to the xcookie tags. + ## runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v3 - - name: Set up Python 3.8 - uses: actions/setup-python@v4.5.0 + uses: actions/checkout@v4.1.1 + - name: Set up Python 3.12 for linting + uses: actions/setup-python@v5.0.0 with: - python-version: 3.8 + python-version: '3.12' - name: Install dependencies run: |- python -m pip install --upgrade pip @@ -29,21 +35,25 @@ jobs: # stop the build if there are Python syntax errors or undefined names flake8 ./guitool_ibeis --count --select=E9,F63,F7,F82 --show-source --statistics build_and_test_sdist: - name: Test sdist Python 3.8 + ## + # Build the pure python package from source and test it in the + # same environment. + ## + name: Build sdist runs-on: ubuntu-latest steps: - name: Checkout source - uses: actions/checkout@v3 - - name: Set up Python 3.8 - uses: actions/setup-python@v4.5.0 + uses: actions/checkout@v4.1.1 + - name: Set up Python 3.12 + uses: actions/setup-python@v5.0.0 with: - python-version: 3.8 + python-version: '3.12' - name: Upgrade pip run: |- python -m pip install --upgrade pip - python -m pip install -r requirements/tests.txt - python -m pip install -r requirements/runtime.txt - python -m pip install -r requirements/headless.txt + python -m pip install --prefer-binary -r requirements/tests.txt + python -m pip install --prefer-binary -r requirements/runtime.txt + python -m pip install --prefer-binary -r requirements/headless.txt - name: Build sdist shell: bash run: |- @@ -52,8 +62,8 @@ jobs: python -m build --sdist --outdir wheelhouse - name: Install sdist run: |- - ls -al ./wheelhouse - pip install wheelhouse/guitool_ibeis*.tar.gz -v + ls -al wheelhouse + pip install --prefer-binary wheelhouse/guitool_ibeis*.tar.gz -v - name: Test minimal loose sdist run: |- pwd @@ -66,13 +76,13 @@ jobs: # Get path to installed package MOD_DPATH=$(python -c "import guitool_ibeis, os; print(os.path.dirname(guitool_ibeis.__file__))") echo "MOD_DPATH = $MOD_DPATH" - python -m pytest --cov={self.mod_name} $MOD_DPATH ../tests + python -m pytest --verbose --cov=guitool_ibeis $MOD_DPATH ../tests cd .. - name: Test full loose sdist run: |- pwd ls -al - python -m pip install -r requirements/headless.txt + python -m pip install --prefer-binary -r requirements/headless.txt # Run in a sandboxed directory WORKSPACE_DNAME="testsrcdir_full_${CI_PYTHON_VERSION}_${GITHUB_RUN_ID}_${RUNNER_OS}" mkdir -p $WORKSPACE_DNAME @@ -81,143 +91,207 @@ jobs: # Get path to installed package MOD_DPATH=$(python -c "import guitool_ibeis, os; print(os.path.dirname(guitool_ibeis.__file__))") echo "MOD_DPATH = $MOD_DPATH" - python -m pytest --cov={self.mod_name} $MOD_DPATH ../tests + python -m pytest --verbose --cov=guitool_ibeis $MOD_DPATH ../tests cd .. - - name: Upload sdist artifact - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v3.1.3 + name: Upload sdist artifact + with: + name: sdist_wheels + path: wheelhouse/*.tar.gz + build_purepy_wheels: + ## + # Download and test the pure-python wheels that were build in the + # build_purepy_wheels and test them in this independent environment. + ## + name: ${{ matrix.python-version }} on ${{ matrix.os }}, arch=${{ matrix.arch }} with ${{ matrix.install-extras }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + python-version: + - '3.12' + arch: + - auto + steps: + - name: Checkout source + uses: actions/checkout@v4.1.1 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + if: runner.os == 'Linux' && matrix.arch != 'auto' + with: + platforms: all + - name: Setup Python + uses: actions/setup-python@v5.0.0 + with: + python-version: ${{ matrix.python-version }} + - name: Build pure wheel + shell: bash + run: |- + python -m pip install setuptools>=0.8 wheel build twine + python -m build --wheel --outdir wheelhouse + python -m twine check ./wheelhouse/guitool_ibeis*.whl + - name: Show built files + shell: bash + run: ls -la wheelhouse + - uses: actions/upload-artifact@v3.1.3 + name: Upload wheels artifact with: name: wheels - path: ./wheelhouse/*.tar.gz - build_and_test_purepy_wheels: + path: ./wheelhouse/guitool_ibeis*.whl + test_purepy_wheels: name: ${{ matrix.python-version }} on ${{ matrix.os }}, arch=${{ matrix.arch }} with ${{ matrix.install-extras }} + if: "! startsWith(github.event.ref, 'refs/heads/release')" runs-on: ${{ matrix.os }} + needs: + - build_purepy_wheels strategy: + fail-fast: false matrix: + # Xcookie generates an explicit list of environments that will be used + # for testing instead of using the more concise matrix notation. include: - - python-version: '3.7' - os: ubuntu-latest + - python-version: '3.8' install-extras: tests-strict,runtime-strict,headless-strict - arch: auto - - python-version: '3.10' os: ubuntu-latest - install-extras: tests-strict,runtime-strict,optional-strict,headless-strict arch: auto - - python-version: '3.7' + - python-version: '3.12' + install-extras: tests-strict,runtime-strict,optional-strict,headless-strict os: ubuntu-latest - install-extras: tests,optional,headless arch: auto - python-version: '3.8' - os: ubuntu-latest install-extras: tests,optional,headless + os: ubuntu-latest arch: auto - python-version: '3.9' - os: ubuntu-latest install-extras: tests,optional,headless + os: ubuntu-latest arch: auto - python-version: '3.10' + install-extras: tests,optional,headless + os: ubuntu-latest + arch: auto + - python-version: '3.11' + install-extras: tests,optional,headless os: ubuntu-latest + arch: auto + - python-version: '3.12' install-extras: tests,optional,headless + os: ubuntu-latest arch: auto steps: - name: Checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4.1.1 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 if: runner.os == 'Linux' && matrix.arch != 'auto' with: platforms: all - name: Setup Python - uses: actions/setup-python@v4.5.0 + uses: actions/setup-python@v5.0.0 with: python-version: ${{ matrix.python-version }} - - name: Build pure wheel + - uses: actions/download-artifact@v2.1.1 + name: Download wheels + with: + name: wheels + path: wheelhouse + - name: Install wheel ${{ matrix.install-extras }} shell: bash + env: + INSTALL_EXTRAS: ${{ matrix.install-extras }} run: |- - python -m pip install pip -U - python -m pip install setuptools>=0.8 build - python -m build --wheel --outdir wheelhouse - - name: Test wheel with ${{ matrix.install-extras }} + echo "Finding the path to the wheel" + ls wheelhouse || echo "wheelhouse does not exist" + echo "Installing helpers" + pip install setuptools>=0.8 setuptools_scm wheel build -U + pip install tomli pkginfo + export WHEEL_FPATH=$(python -c "import pathlib; print(str(sorted(pathlib.Path('wheelhouse').glob('guitool_ibeis*.whl'))[-1]).replace(chr(92), chr(47)))") + export MOD_VERSION=$(python -c "from pkginfo import Wheel; print(Wheel('$WHEEL_FPATH').version)") + echo "$WHEEL_FPATH=WHEEL_FPATH" + echo "$INSTALL_EXTRAS=INSTALL_EXTRAS" + echo "$MOD_VERSION=MOD_VERSION" + pip install --prefer-binary "guitool_ibeis[$INSTALL_EXTRAS]==$MOD_VERSION" -f wheelhouse + echo "Install finished." + - name: Test wheel ${{ matrix.install-extras }} shell: bash env: - INSTALL_EXTRAS: ${{ matrix.install-extras }} CI_PYTHON_VERSION: py${{ matrix.python-version }} run: |- - # Find the path to the wheel - ls wheelhouse - pip install tomli pkginfo - MOD_NAME=guitool_ibeis - echo "MOD_NAME=$MOD_NAME" - WHEEL_FPATH=$(python -c "import pathlib; print(str(sorted(pathlib.Path('wheelhouse').glob('$MOD_NAME*.whl'))[-1]).replace(chr(92), chr(47)))") - echo "WHEEL_FPATH=$WHEEL_FPATH" - MOD_VERSION=$(python -c "from pkginfo import Wheel; print(Wheel('$WHEEL_FPATH').version)") - echo "MOD_VERSION=$MOD_VERSION" - # Install the wheel (ensure we are using the version we just built) - # NOTE: THE VERSION MUST BE NEWER THAN AN EXISTING PYPI VERSION OR THIS MAY FAIL - pip install "$MOD_NAME[$INSTALL_EXTRAS]==$MOD_VERSION" -f wheelhouse - # Create a sandboxed directory - WORKSPACE_DNAME="testdir_${CI_PYTHON_VERSION}_${GITHUB_RUN_ID}_${RUNNER_OS}" + echo "Creating test sandbox directory" + export WORKSPACE_DNAME="testdir_${CI_PYTHON_VERSION}_${GITHUB_RUN_ID}_${RUNNER_OS}" + echo "WORKSPACE_DNAME=$WORKSPACE_DNAME" mkdir -p $WORKSPACE_DNAME + echo "cd-ing into the workspace" cd $WORKSPACE_DNAME + pwd + ls -altr # Get the path to the installed package and run the tests - MOD_DPATH=$(python -c "import guitool_ibeis, os; print(os.path.dirname(guitool_ibeis.__file__))") - echo "MOD_DPATH = $MOD_DPATH" - python -m pytest -p pytester -p no:doctest --xdoctest --cov-config ../pyproject.toml --cov-report term --cov="$MOD_NAME" "$MOD_DPATH" ../tests + export MOD_DPATH=$(python -c "import guitool_ibeis, os; print(os.path.dirname(guitool_ibeis.__file__))") + export MOD_NAME=guitool_ibeis + echo " + --- + MOD_DPATH = $MOD_DPATH + --- + running the pytest command inside the workspace + --- + " + python -m pytest --verbose -p pytester -p no:doctest --xdoctest --cov-config ../pyproject.toml --cov-report term --durations=100 --cov="$MOD_NAME" "$MOD_DPATH" ../tests + echo "pytest command finished, moving the coverage file to the repo root" + ls -al # Move coverage file to a new name mv .coverage "../.coverage.$WORKSPACE_DNAME" + echo "changing directory back to th repo root" cd .. - - name: Show built files - shell: bash - run: ls -la wheelhouse - - name: Set up Python 3.8 to combine coverage Linux - uses: actions/setup-python@v4.5.0 - if: runner.os == 'Linux' - with: - python-version: 3.8 + ls -al - name: Combine coverage Linux if: runner.os == 'Linux' run: |- echo '############ PWD' pwd + cp .wheelhouse/.coverage* . || true ls -al python -m pip install coverage[toml] echo '############ combine' - coverage combine . + coverage combine . || true echo '############ XML' - coverage xml -o ./tests/coverage.xml - echo '############ FIND' - find . -name .coverage.* - find . -name coverage.xml - - uses: codecov/codecov-action@v3 + coverage xml -o ./coverage.xml || true + echo '### The cwd should now have a coverage.xml' + ls -altr + pwd + - uses: codecov/codecov-action@v4.0.1 name: Codecov Upload with: - file: ./tests/coverage.xml - - uses: actions/upload-artifact@v3 - name: Upload wheels artifact - with: - name: wheels - path: ./wheelhouse/guitool_ibeis*.whl + file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} test_deploy: name: Uploading Test to PyPi runs-on: ubuntu-latest if: github.event_name == 'push' && ! startsWith(github.event.ref, 'refs/tags') && ! startsWith(github.event.ref, 'refs/heads/release') needs: + - build_purepy_wheels - build_and_test_sdist - - build_and_test_purepy_wheels steps: - name: Checkout source - uses: actions/checkout@v3 - - uses: actions/download-artifact@v3 - name: Download wheels and sdist + uses: actions/checkout@v4.1.1 + - uses: actions/download-artifact@v2.1.1 + name: Download wheels with: name: wheels path: wheelhouse + - uses: actions/download-artifact@v2.1.1 + name: Download sdist + with: + name: sdist_wheels + path: wheelhouse - name: Show files to upload shell: bash run: ls -la wheelhouse - name: Sign and Publish env: TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/ - TWINE_USERNAME: ${{ secrets.TEST_TWINE_USERNAME }} + TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TEST_TWINE_PASSWORD }} CI_SECRET: ${{ secrets.CI_SECRET }} run: |- @@ -238,29 +312,60 @@ jobs: pip install urllib3 requests[security] twine GPG_KEYID=$(cat dev/public_gpg_key) echo "GPG_KEYID = '$GPG_KEYID'" - DO_GPG=True GPG_KEYID=$GPG_KEYID TWINE_REPOSITORY_URL=${TWINE_REPOSITORY_URL} TWINE_PASSWORD=$TWINE_PASSWORD TWINE_USERNAME=$TWINE_USERNAME GPG_EXECUTABLE=$GPG_EXECUTABLE DO_UPLOAD=True DO_TAG=False ./publish.sh + GPG_SIGN_CMD="$GPG_EXECUTABLE --batch --yes --detach-sign --armor --local-user $GPG_KEYID" + WHEEL_PATHS=(wheelhouse/*.whl wheelhouse/*.tar.gz) + WHEEL_PATHS_STR=$(printf '"%s" ' "${WHEEL_PATHS[@]}") + echo "$WHEEL_PATHS_STR" + for WHEEL_PATH in "${WHEEL_PATHS[@]}" + do + echo "------" + echo "WHEEL_PATH = $WHEEL_PATH" + $GPG_SIGN_CMD --output $WHEEL_PATH.asc $WHEEL_PATH + $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH || echo "hack, the first run of gpg very fails" + $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH + done + ls -la wheelhouse + pip install opentimestamps-client + ots stamp wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.asc + ls -la wheelhouse + twine upload --username __token__ --password "$TWINE_PASSWORD" --repository-url "$TWINE_REPOSITORY_URL" wheelhouse/*.whl wheelhouse/*.tar.gz --skip-existing --verbose || { echo "failed to twine upload" ; exit 1; } + - uses: actions/upload-artifact@v3.1.3 + name: Upload deploy artifacts + with: + name: deploy_artifacts + path: |- + wheelhouse/*.whl + wheelhouse/*.zip + wheelhouse/*.tar.gz + wheelhouse/*.asc + wheelhouse/*.ots live_deploy: name: Uploading Live to PyPi runs-on: ubuntu-latest if: github.event_name == 'push' && (startsWith(github.event.ref, 'refs/tags') || startsWith(github.event.ref, 'refs/heads/release')) needs: + - build_purepy_wheels - build_and_test_sdist - - build_and_test_purepy_wheels steps: - name: Checkout source - uses: actions/checkout@v3 - - uses: actions/download-artifact@v3 - name: Download wheels and sdist + uses: actions/checkout@v4.1.1 + - uses: actions/download-artifact@v2.1.1 + name: Download wheels with: name: wheels path: wheelhouse + - uses: actions/download-artifact@v2.1.1 + name: Download sdist + with: + name: sdist_wheels + path: wheelhouse - name: Show files to upload shell: bash run: ls -la wheelhouse - name: Sign and Publish env: TWINE_REPOSITORY_URL: https://upload.pypi.org/legacy/ - TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} + TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} CI_SECRET: ${{ secrets.CI_SECRET }} run: |- @@ -281,7 +386,78 @@ jobs: pip install urllib3 requests[security] twine GPG_KEYID=$(cat dev/public_gpg_key) echo "GPG_KEYID = '$GPG_KEYID'" - DO_GPG=True GPG_KEYID=$GPG_KEYID TWINE_REPOSITORY_URL=${TWINE_REPOSITORY_URL} TWINE_PASSWORD=$TWINE_PASSWORD TWINE_USERNAME=$TWINE_USERNAME GPG_EXECUTABLE=$GPG_EXECUTABLE DO_UPLOAD=True DO_TAG=False ./publish.sh + GPG_SIGN_CMD="$GPG_EXECUTABLE --batch --yes --detach-sign --armor --local-user $GPG_KEYID" + WHEEL_PATHS=(wheelhouse/*.whl wheelhouse/*.tar.gz) + WHEEL_PATHS_STR=$(printf '"%s" ' "${WHEEL_PATHS[@]}") + echo "$WHEEL_PATHS_STR" + for WHEEL_PATH in "${WHEEL_PATHS[@]}" + do + echo "------" + echo "WHEEL_PATH = $WHEEL_PATH" + $GPG_SIGN_CMD --output $WHEEL_PATH.asc $WHEEL_PATH + $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH || echo "hack, the first run of gpg very fails" + $GPG_EXECUTABLE --verify $WHEEL_PATH.asc $WHEEL_PATH + done + ls -la wheelhouse + pip install opentimestamps-client + ots stamp wheelhouse/*.whl wheelhouse/*.tar.gz wheelhouse/*.asc + ls -la wheelhouse + twine upload --username __token__ --password "$TWINE_PASSWORD" --repository-url "$TWINE_REPOSITORY_URL" wheelhouse/*.whl wheelhouse/*.tar.gz --skip-existing --verbose || { echo "failed to twine upload" ; exit 1; } + - uses: actions/upload-artifact@v3.1.3 + name: Upload deploy artifacts + with: + name: deploy_artifacts + path: |- + wheelhouse/*.whl + wheelhouse/*.zip + wheelhouse/*.tar.gz + wheelhouse/*.asc + wheelhouse/*.ots + release: + name: Create Github Release + if: github.event_name == 'push' && (startsWith(github.event.ref, 'refs/tags') || startsWith(github.event.ref, 'refs/heads/release')) + runs-on: ubuntu-latest + permissions: + contents: write + needs: + - live_deploy + steps: + - name: Checkout source + uses: actions/checkout@v4.1.1 + - uses: actions/download-artifact@v2.1.1 + name: Download artifacts + with: + name: deploy_artifacts + path: wheelhouse + - name: Show files to release + shell: bash + run: ls -la wheelhouse + - run: 'echo "Automatic Release Notes. TODO: improve" > ${{ github.workspace }}-CHANGELOG.txt' + - name: Tag Release Commit + if: (startsWith(github.event.ref, 'refs/heads/release')) + run: |- + export VERSION=$(python -c "import setup; print(setup.VERSION)") + git tag "v$VERSION" + git push origin "v$VERSION" + - uses: softprops/action-gh-release@v1 + name: Create Release + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + body_path: ${{ github.workspace }}-CHANGELOG.txt + tag_name: ${{ github.ref }} + name: Release ${{ github.ref }} + body: Automatic Release + generate_release_notes: true + draft: true + prerelease: false + files: |- + wheelhouse/*.whl + wheelhouse/*.asc + wheelhouse/*.ots + wheelhouse/*.zip + wheelhouse/*.tar.gz ### diff --git a/.readthedocs.yml b/.readthedocs.yml index bd2db10..b7eb0e9 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -7,11 +7,14 @@ # Required version: 2 +build: + os: "ubuntu-22.04" + tools: + python: "3.11" sphinx: configuration: docs/source/conf.py formats: all python: - version: 3.7 install: - requirements: requirements/headless.txt - requirements: requirements/docs.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2741bb8..708718a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ We are currently working on porting this changelog to the specifications in [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Version 2.2.0] - + +### Fixed: +* Removed codecov from test requirements + ## [Version 2.1.2] ### Changed diff --git a/dev/ci_public_gpg_key.pgp.enc b/dev/ci_public_gpg_key.pgp.enc index a316db6..322cc4b 100644 --- a/dev/ci_public_gpg_key.pgp.enc +++ b/dev/ci_public_gpg_key.pgp.enc @@ -1,49 +1,49 @@ -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 -s+AYRSiYgmaHTlCW5qQUGw== +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 +qnL4zG6lfJiMzdjZRl9a1g== diff --git a/dev/ci_secret_gpg_subkeys.pgp.enc b/dev/ci_secret_gpg_subkeys.pgp.enc index 7963b0c..fcebf46 100644 --- a/dev/ci_secret_gpg_subkeys.pgp.enc +++ b/dev/ci_secret_gpg_subkeys.pgp.enc @@ -1,34 +1,34 @@ -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 diff --git a/dev/gpg_owner_trust.enc b/dev/gpg_owner_trust.enc index 6073f32..259ddc5 100644 --- a/dev/gpg_owner_trust.enc +++ b/dev/gpg_owner_trust.enc @@ -1,11 +1,12 @@ -U2FsdGVkX18scN4r/fcHWF44oWk7UF7zmcZ8O6q6OksUZ+WgxB7dJ4vKIifmTKNW -xzGmyXsMnR+qv/PIeDdT7PU2os5beECBSesLt3ZuEas6ZeEoqc/kvDT+3rjjJJrX -wZbsJQ3cw8+IJIKVNp4PAz76UInBihCgmXl0rEA7hlioaQ1rNByvH5VjHlRE+56D -ispZ5+rrY2eO8x6/fMZzwMfwTJ1FH2P2pFi5OPNmIaLMZK7xcq2h9M9EEulazYeN -YjadHL0PSYkKaoTN/D620cNoO3t+Tf3sI11rNURDnx4AYkZOoA4IeQJLuCTarYjI -wf7yyoBld0QhaLmBYRcg1a9VVA54NyaJ9Xfpok0Y/twAc3DPQFoPYMIz0NgOBmJx -kF1PmTQ9mpm/ysSwUecPA1qUnFsfYm44JMr7hAlg2wjnkWFAvv0Y+bJH0nuMbNQ9 -WFHD2Ao5Zj6G597xzNXD+BHykOfiwhMzs/2HgZVKZCt0sMIAmCVWtaO6QlkoR7tw -LIzNxw+yOD0xiUzfhxtD+rcUKONfkRRc+qyydxmQ0XBnlT649FxQ/e52tOFmw4Io -1zlpAk+Re3NkKnddcyQH72N0oUB+XuoGcJEOf52xootZ95/J9ZH8Av3WOzegu920 -c7J3iDVN71WGXeo7fe6kFw== +U2FsdGVkX18NV21kNNBvFsDPaOEvySR107Fiti3an/XunH0u6/GhoIF57PyS4A23 +fyrrtJV9PEwZXzeJ4uyDLdyqLWh1DhLbmsg2+Ywb7sjwGPBIc04FCdCR1dRuGTO7 +XPXYQMSCuxsyaQm0CMpeT5g8i6pOkuzrrkIuDbReJEUtWia4ITxOwYRzu3FlPbj/ +uvm4hUnmH/AgiQlGW9N4y7QKiTUMkrZ3MZZRV1o25EL1XHxqPTayVRURpp8VCRkl +lW1SZ5uZzLxmNDI0rgVZMlZGbaJI1N+oC1hHZdYYyluee2dCueFi4zSJmFlDo53s +Alkd3J8G0AG8HRF6sY5KvCry8p0k5uVuDPxX+4fdF55gm04eVoFoY8L5bb4S5TKp +9AitpUr4GmT98f2i7xHB/gPHi4cG0csislbIPo/ndT4Med9GnB4eIV45A2ewDhQ4 +GEVHlXLs6BqVsN+uKGnE6TjOk7mBkHWmC0yRnzMQECGr2UNFfP2UxEJ8VIB2B+jw +tAoKAntaMBGEALX+TYgjq8oMrtMmKWYd3xaBKSm+QCxIylr5TpBdC07Y7qJJCMqh +NTdgz9w9cg0MkMRKO7qBnxSBaT9/LoaHf2/tONDbYbsUep5WaH7SOxTNCNoInDVp +E0vRCG1wNj0TRXezLVSsm+P7Yfonax9tclqMpbCl0FNYZ19IRhJLb4C3SVHQgzRp +RnhNKBj/+SfgFqaTgc03pA== diff --git a/dev/setup_secrets.sh b/dev/setup_secrets.sh index 1ead971..6321e5a 100644 --- a/dev/setup_secrets.sh +++ b/dev/setup_secrets.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash __doc__=' ============================ SETUP CI SECRET INSTRUCTIONS @@ -25,7 +25,7 @@ following CI platforms is used: GITHUB ACTION INSTRUCTIONS ========================= -* `PERSONAL_GITHUB_PUSH_TOKEN` - +* `PERSONAL_GITHUB_PUSH_TOKEN` - This is only needed if you want to automatically git-tag release branches. To make a API token go to: @@ -47,8 +47,8 @@ GITLAB ACTION INSTRUCTIONS tee /tmp/repl && colordiff .setup_secrets.sh /tmp/repl ``` - * Make sure you add Runners to your project - https://gitlab.org.com/utils/xcookie/-/settings/ci_cd + * Make sure you add Runners to your project + https://gitlab.org.com/utils/xcookie/-/settings/ci_cd in Runners-> Shared Runners and Runners-> Available specific runners @@ -60,16 +60,16 @@ GITLAB ACTION INSTRUCTIONS * TWINE_USERNAME - this is your pypi username twine info is only needed if you want to automatically publish to pypi - * TWINE_PASSWORD - this is your pypi password + * TWINE_PASSWORD - this is your pypi password - * CI_SECRET - We will use this as a secret key to encrypt/decrypt gpg secrets + * CI_SECRET - We will use this as a secret key to encrypt/decrypt gpg secrets This is only needed if you want to automatically sign published wheels with a gpg key. - * GITLAB_ORG_PUSH_TOKEN - + * GITLAB_ORG_PUSH_TOKEN - This is only needed if you want to automatically git-tag release branches. - Create a new personal access token in User->Settings->Tokens, + Create a new personal access token in User->Settings->Tokens, You can name the token GITLAB_ORG_PUSH_TOKEN_VALUE Give it api and write repository permissions @@ -165,8 +165,10 @@ setup_package_environs_github_pyutils(){ upload_github_secrets(){ load_secrets unset GITHUB_TOKEN - #printf "%s" "$GITHUB_TOKEN" | gh auth login --hostname Github.com --with-token - gh auth login + #printf "%s" "$GITHUB_TOKEN" | gh auth login --hostname Github.com --with-token + if ! gh auth status ; then + gh auth login + fi source dev/secrets_configuration.sh gh secret set "TWINE_USERNAME" -b"${!VARNAME_TWINE_USERNAME}" gh secret set "TEST_TWINE_USERNAME" -b"${!VARNAME_TEST_TWINE_USERNAME}" @@ -223,15 +225,15 @@ upload_gitlab_group_secrets(){ TMP_DIR=$(mktemp -d -t ci-XXXXXXXXXX) curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups" > "$TMP_DIR/all_group_info" - GROUP_ID=$(cat "$TMP_DIR/all_group_info" | jq ". | map(select(.path==\"$GROUP_NAME\")) | .[0].id") + GROUP_ID=$(< "$TMP_DIR/all_group_info" jq ". | map(select(.path==\"$GROUP_NAME\")) | .[0].id") echo "GROUP_ID = $GROUP_ID" curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID" > "$TMP_DIR/group_info" - cat "$TMP_DIR/group_info" | jq + < "$TMP_DIR/group_info" jq # Get group-level secret variables curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID/variables" > "$TMP_DIR/group_vars" - cat "$TMP_DIR/group_vars" | jq '.[] | .key' + < "$TMP_DIR/group_vars" jq '.[] | .key' if [[ "$?" != "0" ]]; then echo "Failed to access group level variables. Probably a permission issue" @@ -244,7 +246,7 @@ upload_gitlab_group_secrets(){ echo "" echo " ---- " LOCAL_VALUE=${!SECRET_VARNAME} - REMOTE_VALUE=$(cat "$TMP_DIR/group_vars" | jq -r ".[] | select(.key==\"$SECRET_VARNAME\") | .value") + REMOTE_VALUE=$(< "$TMP_DIR/group_vars" jq -r ".[] | select(.key==\"$SECRET_VARNAME\") | .value") # Print current local and remote value of a variable echo "SECRET_VARNAME_PTR = $SECRET_VARNAME_PTR" @@ -264,14 +266,14 @@ upload_gitlab_group_secrets(){ --form "protected=true" \ --form "masked=true" \ --form "environment_scope=*" \ - --form "variable_type=env_var" + --form "variable_type=env_var" toggle_setx_exit elif [[ "$REMOTE_VALUE" != "$LOCAL_VALUE" ]]; then echo "Remove variable does not agree, putting" # Update variable value toggle_setx_enter curl --request PUT --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID/variables/$SECRET_VARNAME" \ - --form "value=${LOCAL_VALUE}" + --form "value=${LOCAL_VALUE}" toggle_setx_exit else echo "Remote value agrees with local" @@ -305,23 +307,23 @@ upload_gitlab_repo_secrets(){ toggle_setx_enter curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups" > "$TMP_DIR/all_group_info" toggle_setx_exit - GROUP_ID=$(cat "$TMP_DIR/all_group_info" | jq ". | map(select(.path==\"$GROUP_NAME\")) | .[0].id") + GROUP_ID=$(< "$TMP_DIR/all_group_info" jq ". | map(select(.path==\"$GROUP_NAME\")) | .[0].id") echo "GROUP_ID = $GROUP_ID" toggle_setx_enter curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID" > "$TMP_DIR/group_info" toggle_setx_exit - GROUP_ID=$(cat "$TMP_DIR/all_group_info" | jq ". | map(select(.path==\"$GROUP_NAME\")) | .[0].id") - cat "$TMP_DIR/group_info" | jq + GROUP_ID=$(< "$TMP_DIR/all_group_info" jq ". | map(select(.path==\"$GROUP_NAME\")) | .[0].id") + < "$TMP_DIR/group_info" jq - PROJECT_ID=$(cat "$TMP_DIR/group_info" | jq ".projects | map(select(.path==\"$PROJECT_NAME\")) | .[0].id") + PROJECT_ID=$(< "$TMP_DIR/group_info" jq ".projects | map(select(.path==\"$PROJECT_NAME\")) | .[0].id") echo "PROJECT_ID = $PROJECT_ID" # Get group-level secret variables toggle_setx_enter curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/projects/$PROJECT_ID/variables" > "$TMP_DIR/project_vars" toggle_setx_exit - cat "$TMP_DIR/project_vars" | jq '.[] | .key' + < "$TMP_DIR/project_vars" jq '.[] | .key' if [[ "$?" != "0" ]]; then echo "Failed to access project level variables. Probably a permission issue" fi @@ -334,7 +336,7 @@ upload_gitlab_repo_secrets(){ echo "" echo " ---- " LOCAL_VALUE=${!SECRET_VARNAME} - REMOTE_VALUE=$(cat "$TMP_DIR/project_vars" | jq -r ".[] | select(.key==\"$SECRET_VARNAME\") | .value") + REMOTE_VALUE=$(< "$TMP_DIR/project_vars" jq -r ".[] | select(.key==\"$SECRET_VARNAME\") | .value") # Print current local and remote value of a variable echo "SECRET_VARNAME_PTR = $SECRET_VARNAME_PTR" @@ -353,7 +355,7 @@ upload_gitlab_repo_secrets(){ --form "protected=true" \ --form "masked=true" \ --form "environment_scope=*" \ - --form "variable_type=env_var" + --form "variable_type=env_var" else echo "dry run, not posting" fi @@ -362,7 +364,7 @@ upload_gitlab_repo_secrets(){ # Update variable value if [[ "$LIVE_MODE" == "1" ]]; then curl --request PUT --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/projects/$PROJECT_ID/variables/$SECRET_VARNAME" \ - --form "value=${LOCAL_VALUE}" + --form "value=${LOCAL_VALUE}" else echo "dry run, not putting" fi @@ -405,7 +407,7 @@ export_encrypted_code_signing_keys(){ echo "MAIN_GPG_KEYID = $MAIN_GPG_KEYID" echo "GPG_SIGN_SUBKEY = $GPG_SIGN_SUBKEY" - # Only export the signing secret subkey + # Only export the signing secret subkey # Export plaintext gpg public keys, private sign key, and trust info mkdir -p dev gpg --armor --export-options export-backup --export-secret-subkeys "${GPG_SIGN_SUBKEY}!" > dev/ci_secret_gpg_subkeys.pgp @@ -421,7 +423,7 @@ export_encrypted_code_signing_keys(){ # Test decrpyt GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/ci_public_gpg_key.pgp.enc | gpg --list-packets --verbose GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/ci_secret_gpg_subkeys.pgp.enc | gpg --list-packets --verbose - GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/gpg_owner_trust.enc + GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/gpg_owner_trust.enc cat dev/public_gpg_key unload_secrets @@ -451,14 +453,14 @@ _test_gnu(){ source dev/secrets_configuration.sh gpg -k - + load_secrets CI_SECRET="${!VARNAME_CI_SECRET}" echo "CI_SECRET = $CI_SECRET" cat dev/public_gpg_key - GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/ci_public_gpg_key.pgp.enc - GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/gpg_owner_trust.enc + GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/ci_public_gpg_key.pgp.enc + GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/gpg_owner_trust.enc GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/ci_secret_gpg_subkeys.pgp.enc GLKWS=$CI_SECRET openssl enc -aes-256-cbc -pbkdf2 -md SHA512 -pass env:GLKWS -d -a -in dev/ci_public_gpg_key.pgp.enc | gpg --import diff --git a/docs/source/conf.py b/docs/source/conf.py index d3b274c..bf6ca29 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,11 +1,15 @@ """ Notes: + Based on template code in: + ~/code/xcookie/xcookie/builders/docs.py + ~/code/xcookie/xcookie/rc/conf_ext.py + http://docs.readthedocs.io/en/latest/getting_started.html pip install sphinx sphinx-autobuild sphinx_rtd_theme sphinxcontrib-napoleon cd ~/code/guitool_ibeis - mkdir docs + mkdir -p docs cd docs sphinx-quickstart @@ -13,37 +17,79 @@ # need to edit the conf.py cd ~/code/guitool_ibeis/docs - sphinx-apidoc -f -o ~/code/guitool_ibeis/docs/source ~/code/guitool_ibeis/guitool_ibeis --separate + sphinx-apidoc --private --separate -f -o ~/code/guitool_ibeis/docs/source/auto ~/code/guitool_ibeis/guitool_ibeis + + # Note: the module should importable before running this + # (e.g. install it in developer mode or munge the PYTHONPATH) make html + git add source/auto/*.rst + Also: To turn on PR checks https://docs.readthedocs.io/en/stable/guides/autobuild-docs-for-pull-requests.html - https://readthedocs.org/dashboard/guitool_ibeis/advanced/ + https://readthedocs.org/dashboard/guitool-ibeis/advanced/ ensure your github account is connected to readthedocs https://readthedocs.org/accounts/social/connections/ ### For gitlab + To enable the read-the-docs go to https://readthedocs.org/dashboard/ and login + The user will need to enable the repo on their readthedocs account: https://readthedocs.org/dashboard/import/manual/? - To enable the read-the-docs go to https://readthedocs.org/dashboard/ and login + Enter the following information: + Set the Repository NAME: guitool_ibeis + Set the Repository URL: https://github.com/Erotemic/guitool_ibeis Make sure you have a .readthedocs.yml file - Click import project: (for github you can select, but gitlab you need to import manually) - Set the Repository NAME: $REPO_NAME - Set the Repository URL: $REPO_URL + For gitlab you also need to setup an integrations. Navigate to: + + https://readthedocs.org/dashboard/guitool-ibeis/integrations/create/ + + Then add gitlab incoming webhook and copy the URL (make sure + you copy the real url and not the text so https is included), + specifically: + + In the "Integration type:" dropdown menu, select + "Gitlab incoming webhook" + + Click "Add integration" + + Copy the text in the "Webhook URL" box to be used later. + + Copy the text in the "Secret" box to be used later. + + Then go to + + https://github.com/Erotemic/guitool_ibeis/hooks + + Click "Add new webhook". + + Copy the text previously saved from the "Webhook URL" box + in the readthedocs form into the "URL" box in the gitlab + form. + + Copy the text previously saved from the "Secret" box + in the readthedocs form into the "Secret token" box in the + gitlab form. + + For trigger permissions select the following checkboxes: + push events, + tag push events, + merge request events + + Click the "Add webhook" button. - For gitlab you also need to setup an integrations and add gitlab - incoming webhook Then go to $REPO_URL/hooks and add the URL + See Docs for more details https://docs.readthedocs.io/en/stable/integrations.html Will also need to activate the main branch: - https://readthedocs.org/projects/guitool_ibeis/versions/ + https://readthedocs.org/projects/guitool-ibeis/versions/ """ # # Configuration file for the Sphinx documentation builder. @@ -90,14 +136,19 @@ def visit_Assign(self, node): return visitor.version project = 'guitool_ibeis' -copyright = '2022, Jon Crall' +copyright = '2024, Jon Crall' author = 'Jon Crall' modname = 'guitool_ibeis' -modpath = join(dirname(dirname(dirname(__file__))), modname, '__init__.py') +repo_dpath = dirname(dirname(dirname(__file__))) +mod_dpath = join(repo_dpath, 'guitool_ibeis') +src_dpath = dirname(mod_dpath) +modpath = join(mod_dpath, '__init__.py') release = parse_version(modpath) version = '.'.join(release.split('.')[0:2]) +# Hack to ensure the module is importable +# sys.path.insert(0, os.path.abspath(src_dpath)) # -- General configuration --------------------------------------------------- @@ -109,13 +160,18 @@ def visit_Assign(self, node): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + # 'autoapi.extension', 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.napoleon', + 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx', + 'sphinx.ext.napoleon', 'sphinx.ext.todo', - 'sphinx.ext.autosummary', - # 'myst_parser', # TODO + 'sphinx.ext.viewcode', + 'myst_parser', # For markdown docs + 'sphinx.ext.imgconverter', # For building latexpdf + 'sphinx.ext.githubpages', + # 'sphinxcontrib.redirects', + 'sphinx_reredirects', ] todo_include_todos = True @@ -123,11 +179,37 @@ def visit_Assign(self, node): napoleon_use_param = False napoleon_use_ivar = True +#autoapi_type = 'python' +#autoapi_dirs = [mod_dpath] + autodoc_inherit_docstrings = False +# Hack for geowatch, todo configure +autosummary_mock_imports = [ + 'geowatch.utils.lightning_ext._jsonargparse_ext_ge_4_24_and_lt_4_xx', + 'geowatch.utils.lightning_ext._jsonargparse_ext_ge_4_22_and_lt_4_24', + 'geowatch.utils.lightning_ext._jsonargparse_ext_ge_4_21_and_lt_4_22', + 'geowatch.tasks.fusion.datamodules.temporal_sampling.affinity_sampling', + 'geowatch.tasks.depth_pcd.model', + 'geowatch.tasks.cold.export_change_map', +] + autodoc_member_order = 'bysource' +autoclass_content = 'both' # autodoc_mock_imports = ['torch', 'torchvision', 'visdom'] +# autoapi_modules = { +# modname: { +# 'override': False, +# 'output': 'auto' +# } +# } +# autoapi_dirs = [f'../../src/{modname}'] +# autoapi_keep_files = True + +# References: +# https://stackoverflow.com/questions/21538983/specifying-targets-for-intersphinx-links-to-numpy-scipy-and-matplotlib + intersphinx_mapping = { # 'pytorch': ('http://pytorch.org/docs/master/', None), 'python': ('https://docs.python.org/3', None), @@ -144,7 +226,24 @@ def visit_Assign(self, node): 'xdoctest': ('https://xdoctest.readthedocs.io/en/latest/', None), 'networkx': ('https://networkx.org/documentation/stable/', None), 'scriptconfig': ('https://scriptconfig.readthedocs.io/en/latest/', None), - + 'rich': ('https://rich.readthedocs.io/en/latest/', None), + + 'numpy': ('https://numpy.org/doc/stable/', None), + 'sympy': ('https://docs.sympy.org/latest/', None), + 'scikit-learn': ('https://scikit-learn.org/stable/', None), + 'pandas': ('https://pandas.pydata.org/docs/', None), + 'matplotlib': ('https://matplotlib.org/stable/', None), + + 'pytest': ('https://docs.pytest.org/en/latest/', None), + 'platformdirs': ('https://platformdirs.readthedocs.io/en/latest/', None), + + 'timerit': ('https://timerit.readthedocs.io/en/latest/', None), + 'progiter': ('https://progiter.readthedocs.io/en/latest/', None), + 'dateutil': ('https://dateutil.readthedocs.io/en/latest/', None), + # 'pytest._pytest.doctest': ('https://docs.pytest.org/en/latest/_modules/_pytest/doctest.html', None), + # 'colorama': ('https://pypi.org/project/colorama/', None), + # 'cv2' : ('http://docs.opencv.org/2.4/', None), + # 'h5py' : ('http://docs.h5py.org/en/latest/', None) } __dev_note__ = """ python -m sphinx.ext.intersphinx https://docs.python.org/3/objects.inv @@ -154,6 +253,11 @@ def visit_Assign(self, node): python -m sphinx.ext.intersphinx https://kwimage.readthedocs.io/en/latest/objects.inv python -m sphinx.ext.intersphinx https://ubelt.readthedocs.io/en/latest/objects.inv python -m sphinx.ext.intersphinx https://networkx.org/documentation/stable/objects.inv + +sphobjinv suggest -t 90 -u https://readthedocs.org/projects/pytest/reference/objects.inv +"signal.convolve2d" + +python -m sphinx.ext.intersphinx https://pygments-doc.readthedocs.io/en/latest/objects.inv """ @@ -199,6 +303,7 @@ def visit_Assign(self, node): html_theme_options = { 'collapse_navigation': False, 'display_version': True, + 'navigation_depth': -1, # 'logo_only': True, } # html_logo = '.static/guitool_ibeis.svg' @@ -223,11 +328,26 @@ def visit_Assign(self, node): # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'guitool_ibeisdoc' +htmlhelp_basename = project + 'doc' # -- Options for LaTeX output ------------------------------------------------ +# References: +# https://tex.stackexchange.com/questions/546246/centos-8-the-font-freeserif-cannot-be-found + +""" +# https://www.sphinx-doc.org/en/master/usage/builders/index.html#sphinx.builders.latex.LaTeXBuilder +# https://tex.stackexchange.com/a/570691/83399 +sudo apt install fonts-freefont-otf texlive-luatex texlive-latex-extra texlive-fonts-recommended texlive-latex-recommended tex-gyre latexmk +make latexpdf LATEXMKOPTS="-shell-escape --synctex=-1 -src-specials -interaction=nonstopmode" +make latexpdf LATEXMKOPTS="-lualatex -interaction=nonstopmode" +make LATEXMKOPTS="-lualatex -interaction=nonstopmode" + +""" +# latex_engine = 'lualatex' +# latex_engine = 'xelatex' + latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # @@ -278,116 +398,636 @@ def visit_Assign(self, node): # -- Extension configuration ------------------------------------------------- - - from sphinx.domains.python import PythonDomain # NOQA # from sphinx.application import Sphinx # NOQA from typing import Any, List # NOQA +# HACK TO PREVENT EXCESSIVE TIME. +# TODO: FIXME FOR REAL +MAX_TIME_MINUTES = None +if MAX_TIME_MINUTES: + import ubelt # NOQA + TIMER = ubelt.Timer() + TIMER.tic() + + class PatchedPythonDomain(PythonDomain): """ References: https://github.com/sphinx-doc/sphinx/issues/3866 """ def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): - # TODO: can use this to resolve references nicely - # if target.startswith('ub.'): - # target = 'ubelt.' + target[3] + """ + Helps to resolves cross-references + """ + if target.startswith('ub.'): + target = 'ubelt.' + target[3] + if target.startswith('xdoc.'): + target = 'xdoctest.' + target[3] return_value = super(PatchedPythonDomain, self).resolve_xref( env, fromdocname, builder, typ, target, node, contnode) return return_value -def process(app, what_: str, name: str, obj: Any, options: Any, lines: - List[str]) -> None: +class GoogleStyleDocstringProcessor: + """ + A small extension that runs after napoleon and reformats erotemic-flavored + google-style docstrings for sphinx. + """ + + def __init__(self, autobuild=1): + self.debug = 0 + self.registry = {} + if autobuild: + self._register_builtins() + + def register_section(self, tag, alias=None): + """ + Decorator that adds a custom processing function for a non-standard + google style tag. The decorated function should accept a list of + docstring lines, where the first one will be the google-style tag that + likely needs to be replaced, and then return the appropriate sphinx + format (TODO what is the name? Is it just RST?). + """ + alias = [] if alias is None else alias + alias = [alias] if not isinstance(alias, (list, tuple, set)) else alias + alias.append(tag) + alias = tuple(alias) + # TODO: better tag patterns + def _wrap(func): + self.registry[tag] = { + 'tag': tag, + 'alias': alias, + 'func': func, + } + return func + return _wrap + + def _register_builtins(self): + """ + Adds definitions I like of CommandLine, TextArt, and Ignore + """ + + @self.register_section(tag='CommandLine') + def commandline(lines): + new_lines = [] + new_lines.append('.. rubric:: CommandLine') + new_lines.append('') + new_lines.append('.. code-block:: bash') + new_lines.append('') + new_lines.extend(lines[1:]) + return new_lines + + @self.register_section(tag='SpecialExample', alias=['Benchmark', 'Sympy', 'Doctest']) + def benchmark(lines): + import textwrap + new_lines = [] + tag = lines[0].replace(':', '').strip() + # new_lines.append(lines[0]) # TODO: it would be nice to change the tagline. + # new_lines.append('') + new_lines.append('.. rubric:: {}'.format(tag)) + new_lines.append('') + new_text = textwrap.dedent('\n'.join(lines[1:])) + redone = new_text.split('\n') + new_lines.extend(redone) + # import ubelt as ub + # print('new_lines = {}'.format(ub.urepr(new_lines, nl=1))) + # new_lines.append('') + return new_lines + + @self.register_section(tag='TextArt', alias=['Ascii']) + def text_art(lines): + new_lines = [] + new_lines.append('.. rubric:: TextArt') + new_lines.append('') + new_lines.append('.. code-block:: bash') + new_lines.append('') + new_lines.extend(lines[1:]) + return new_lines + + # @self.register_section(tag='TODO', alias=['.. todo::']) + # def todo_section(lines): + # """ + # Fixup todo sections + # """ + # import xdev + # xdev.embed() + # import ubelt as ub + # print('lines = {}'.format(ub.urepr(lines, nl=1))) + # return new_lines + + @self.register_section(tag='Ignore') + def ignore(lines): + return [] + + def process(self, lines): + """ + Example: + >>> import ubelt as ub + >>> self = GoogleStyleDocstringProcessor() + >>> lines = ['Hello world', + >>> '', + >>> 'CommandLine:', + >>> ' hi', + >>> '', + >>> 'CommandLine:', + >>> '', + >>> ' bye', + >>> '', + >>> 'TextArt:', + >>> '', + >>> ' 1', + >>> ' 2', + >>> '', + >>> ' 345', + >>> '', + >>> 'Foobar:', + >>> '', + >>> 'TextArt:'] + >>> new_lines = self.process(lines[:]) + >>> print(chr(10).join(new_lines)) + """ + orig_lines = lines[:] + new_lines = [] + curr_mode = '__doc__' + accum = [] + + def accept(): + """ called when we finish reading a section """ + if curr_mode == '__doc__': + # Keep the lines as-is + new_lines.extend(accum) + else: + # Process this section with the given function + regitem = self.registry[curr_mode] + func = regitem['func'] + fixed = func(accum) + new_lines.extend(fixed) + # Reset the accumulator for the next section + accum[:] = [] + + for line in orig_lines: + + found = None + for regitem in self.registry.values(): + if line.startswith(regitem['alias']): + found = regitem['tag'] + break + if not found and line and not line.startswith(' '): + # if the line startswith anything but a space, we are no longer + # in the previous nested scope. NOTE: This assumption may not + # be general, but it works for my code. + found = '__doc__' + + if found: + # New section is found, accept the previous one and start + # accumulating the new one. + accept() + curr_mode = found + + accum.append(line) + + # Finialize the last section + accept() + + lines[:] = new_lines + # make sure there is a blank line at the end + if lines and lines[-1]: + lines.append('') + + return lines + + def process_docstring_callback(self, app, what_: str, name: str, obj: Any, + options: Any, lines: List[str]) -> None: + """ + Callback to be registered to autodoc-process-docstring + + Custom process to transform docstring lines Remove "Ignore" blocks + + Args: + app (sphinx.application.Sphinx): the Sphinx application object + + what (str): + the type of the object which the docstring belongs to (one of + "module", "class", "exception", "function", "method", "attribute") + + name (str): the fully qualified name of the object + + obj: the object itself + + options: the options given to the directive: an object with + attributes inherited_members, undoc_members, show_inheritance + and noindex that are true if the flag option of same name was + given to the auto directive + + lines (List[str]): the lines of the docstring, see above + + References: + https://www.sphinx-doc.org/en/1.5.1/_modules/sphinx/ext/autodoc.html + https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html + """ + if self.debug: + print(f'ProcessDocstring: name={name}, what_={what_}, num_lines={len(lines)}') + + # print('BEFORE:') + # import ubelt as ub + # print('lines = {}'.format(ub.urepr(lines, nl=1))) + + self.process(lines) + + # docstr = '\n'.join(lines) + # if 'Convert the Mask' in docstr: + # import xdev + # xdev.embed() + + # if 'keys in this dictionary ' in docstr: + # import xdev + # xdev.embed() + + render_doc_images = 0 + + if MAX_TIME_MINUTES and TIMER.toc() > (60 * MAX_TIME_MINUTES): + render_doc_images = False # FIXME too slow on RTD + + if render_doc_images: + # DEVELOPING + if any('REQUIRES(--show)' in line for line in lines): + # import xdev + # xdev.embed() + create_doctest_figure(app, obj, name, lines) + + FIX_EXAMPLE_FORMATTING = 1 + if FIX_EXAMPLE_FORMATTING: + for idx, line in enumerate(lines): + if line == "Example:": + lines[idx] = "**Example:**" + lines.insert(idx + 1, "") + + REFORMAT_SECTIONS = 0 + if REFORMAT_SECTIONS: + REFORMAT_RETURNS = 0 + REFORMAT_PARAMS = 0 + + docstr = SphinxDocstring(lines) + + if REFORMAT_PARAMS: + for found in docstr.find_tagged_lines('Parameters'): + print(found['text']) + edit_slice = found['edit_slice'] + + # TODO: figure out how to do this. + + # # file = 'foo.rst' + # import rstparse + # rst = rstparse.Parser() + # import io + # rst.read(io.StringIO(found['text'])) + # rst.parse() + # for line in rst.lines: + # print(line) + + # # found['text'] + # import docutils + + # settings = docutils.frontend.OptionParser( + # components=(docutils.parsers.rst.Parser,) + # ).get_default_values() + # document = docutils.utils.new_document('', settings) + # from docutils.parsers import rst + # rst.Parser().parse(found['text'], document) + + if REFORMAT_RETURNS: + for found in docstr.find_tagged_lines('returns'): + # FIXME: account for new slice with -2 offset + edit_slice = found['edit_slice'] + text = found['text'] + new_lines = [] + for para in text.split('\n\n'): + indent = para[:len(para) - len(para.lstrip())] + new_paragraph = indent + paragraph(para) + new_lines.append(new_paragraph) + new_lines.append('') + new_lines = new_lines[:-1] + lines[edit_slice] = new_lines + + # print('AFTER:') + # print('lines = {}'.format(ub.urepr(lines, nl=1))) + + # if name == 'kwimage.Affine.translate': + # import sys + # sys.exit(1) + + +class SphinxDocstring: """ - Custom process to transform docstring lines Remove "Ignore" blocks + Helper to parse and modify sphinx docstrings + """ + def __init__(docstr, lines): + docstr.lines = lines + + # FORMAT THE RETURNS SECTION A BIT NICER + import re + tag_pat = re.compile(r'^:(\w*):') + directive_pat = re.compile(r'^.. (\w*)::\s*(\w*)') + + # Split by sphinx types, mark the line offset where they start / stop + sphinx_parts = [] + for idx, line in enumerate(lines): + tag_match = tag_pat.search(line) + directive_match = directive_pat.search(line) + if tag_match: + tag = tag_match.groups()[0] + sphinx_parts.append({ + 'tag': tag, 'start_offset': idx, + 'type': 'tag', + }) + elif directive_match: + tag = directive_match.groups()[0] + sphinx_parts.append({ + 'tag': tag, 'start_offset': idx, + 'type': 'directive', + }) + + prev_offset = len(lines) + for part in sphinx_parts[::-1]: + part['end_offset'] = prev_offset + prev_offset = part['start_offset'] + + docstr.sphinx_parts = sphinx_parts + + if 0: + for line in lines: + print(line) + + def find_tagged_lines(docstr, tag): + for part in docstr.sphinx_parts[::-1]: + if part['tag'] == tag: + edit_slice = slice(part['start_offset'], part['end_offset']) + return_section = docstr.lines[edit_slice] + text = '\n'.join(return_section) + found = { + 'edit_slice': edit_slice, + 'text': text, + } + yield found + + +def paragraph(text): + r""" + Wraps multi-line strings and restructures the text to remove all newlines, + heading, trailing, and double spaces. + + Useful for writing log messages Args: - app (sphinx.application.Sphinx): the Sphinx application object + text (str): typically a multiline string - what (str): - the type of the object which the docstring belongs to (one of - "module", "class", "exception", "function", "method", "attribute") + Returns: + str: the reduced text block + """ + import re + out = re.sub(r'\s\s*', ' ', text).strip() + return out - name (str): the fully qualified name of the object - obj: the object itself +def create_doctest_figure(app, obj, name, lines): + """ + The idea is that each doctest that produces a figure should generate that + and then that figure should be part of the docs. + """ + import xdoctest + import sys + import types + if isinstance(obj, types.ModuleType): + module = obj + else: + module = sys.modules[obj.__module__] + # TODO: read settings from pyproject.toml? + if '--show' not in sys.argv: + sys.argv.append('--show') + if '--nointeract' not in sys.argv: + sys.argv.append('--nointeract') + modpath = module.__file__ + + # print(doctest.format_src()) + import pathlib + # HACK: write to the srcdir + doc_outdir = pathlib.Path(app.outdir) + doc_srcdir = pathlib.Path(app.srcdir) + doc_static_outdir = doc_outdir / '_static' + doc_static_srcdir = doc_srcdir / '_static' + src_fig_dpath = (doc_static_srcdir / 'images') + src_fig_dpath.mkdir(exist_ok=True, parents=True) + out_fig_dpath = (doc_static_outdir / 'images') + out_fig_dpath.mkdir(exist_ok=True, parents=True) + + # fig_dpath = (doc_outdir / 'autofigs' / name).mkdir(exist_ok=True) + + fig_num = 1 + + import kwplot + kwplot.autompl(force='agg') + plt = kwplot.autoplt() + + docstr = '\n'.join(lines) + + # TODO: The freeform parser does not work correctly here. + # We need to parse out the sphinx (epdoc)? individual examples + # so we can get different figures. But we can hack it for now. + + import re + split_parts = re.split('({}\\s*\n)'.format(re.escape('.. rubric:: Example')), docstr) + # split_parts = docstr.split('.. rubric:: Example') + + # import xdev + # xdev.embed() + + def doctest_line_offsets(doctest): + # Where the doctests starts and ends relative to the file + start_line_offset = doctest.lineno - 1 + last_part = doctest._parts[-1] + last_line_offset = start_line_offset + last_part.line_offset + last_part.n_lines - 1 + offsets = { + 'start': start_line_offset, + 'end': last_line_offset, + 'stop': last_line_offset + 1, + } + return offsets + + # from xdoctest import utils + # part_lines = utils.add_line_numbers(docstr.split('\n'), n_digits=3, start=0) + # print('\n'.join(part_lines)) + + to_insert_fpaths = [] + curr_line_offset = 0 + for part in split_parts: + num_lines = part.count('\n') + + doctests = list(xdoctest.core.parse_docstr_examples( + part, modpath=modpath, callname=name, + # style='google' + )) + # print(doctests) + + # doctests = list(xdoctest.core.parse_docstr_examples( + # docstr, modpath=modpath, callname=name)) + + for doctest in doctests: + if '--show' in part: + ... + # print('-- SHOW TEST---')/) + # kwplot.close_figures() + try: + import pytest # NOQA + except ImportError: + pass + try: + from xdoctest.exceptions import Skipped + except ImportError: # nocover + # Define dummy skipped exception if pytest is not available + class Skipped(Exception): + pass + try: + doctest.mode = 'native' + doctest.run(verbose=0, on_error='raise') + ... + except Skipped: + print(f'Skip doctest={doctest}') + except Exception as ex: + print(f'ex={ex}') + print(f'Error in doctest={doctest}') + + offsets = doctest_line_offsets(doctest) + doctest_line_end = curr_line_offset + offsets['stop'] + insert_line_index = doctest_line_end + + figures = kwplot.all_figures() + for fig in figures: + fig_num += 1 + # path_name = path_sanatize(name) + path_name = (name).replace('.', '_') + fig_fpath = src_fig_dpath / f'fig_{path_name}_{fig_num:03d}.jpeg' + fig.savefig(fig_fpath) + print(f'Wrote figure: {fig_fpath}') + to_insert_fpaths.append({ + 'insert_line_index': insert_line_index, + 'fpath': fig_fpath, + }) + + for fig in figures: + plt.close(fig) + # kwplot.close_figures(figures) + + curr_line_offset += (num_lines) + + # if len(doctests) > 1: + # doctests + # import xdev + # xdev.embed() - options: the options given to the directive: an object with - attributes inherited_members, undoc_members, show_inheritance - and noindex that are true if the flag option of same name was - given to the auto directive + INSERT_AT = 'end' + INSERT_AT = 'inline' - lines (List[str]): the lines of the docstring, see above + end_index = len(lines) + # Reverse order for inserts + import shutil + for info in to_insert_fpaths[::-1]: + src_abs_fpath = info['fpath'] - References: - https://www.sphinx-doc.org/en/1.5.1/_modules/sphinx/ext/autodoc.html - https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html - """ - # if what and what_ not in what: - # return - orig_lines = lines[:] + rel_to_static_fpath = src_abs_fpath.relative_to(doc_static_srcdir) + # dst_abs_fpath = doc_static_outdir / rel_to_static_fpath + # dst_abs_fpath.parent.mkdir(parents=True, exist_ok=True) - # text = '\n'.join(lines) - # if 'Example' in text and 'CommandLine' in text: - # import xdev - # xdev.embed() + rel_to_root_fpath = src_abs_fpath.relative_to(doc_srcdir) - ignore_tags = tuple(['Ignore']) + dst_abs_fpath1 = doc_outdir / rel_to_root_fpath + dst_abs_fpath1.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(src_abs_fpath, dst_abs_fpath1) - mode = None - # buffer = None - new_lines = [] - for i, line in enumerate(orig_lines): - - # See if the line triggers a mode change - if line.startswith(ignore_tags): - mode = 'ignore' - elif line.startswith('CommandLine'): - mode = 'cmdline' - elif line and not line.startswith(' '): - # if the line startswith anything but a space, we are no - # longer in the previous nested scope - mode = None - - if mode is None: - new_lines.append(line) - elif mode == 'ignore': - # print('IGNORE line = {!r}'.format(line)) - pass - elif mode == 'cmdline': - if line.startswith('CommandLine'): - new_lines.append('.. rubric:: CommandLine') - new_lines.append('') - new_lines.append('.. code-block:: bash') - new_lines.append('') - # new_lines.append(' # CommandLine') - else: - # new_lines.append(line.strip()) - new_lines.append(line) + dst_abs_fpath2 = doc_outdir / rel_to_static_fpath + dst_abs_fpath2.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(src_abs_fpath, dst_abs_fpath2) + + dst_abs_fpath3 = doc_srcdir / rel_to_static_fpath + dst_abs_fpath3.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(src_abs_fpath, dst_abs_fpath3) + + if INSERT_AT == 'inline': + # Try to insert after test + insert_index = info['insert_line_index'] + elif INSERT_AT == 'end': + insert_index = end_index else: - raise KeyError(mode) + raise KeyError(INSERT_AT) + lines.insert(insert_index, '.. image:: {}'.format('..' / rel_to_root_fpath)) + # lines.insert(insert_index, '.. image:: {}'.format(rel_to_root_fpath)) + # lines.insert(insert_index, '.. image:: {}'.format(rel_to_static_fpath)) + lines.insert(insert_index, '') + - lines[:] = new_lines - # make sure there is a blank line at the end - if lines and lines[-1]: - lines.append('') +def postprocess_hyperlinks(app, doctree, docname): + """ + Extension to fixup hyperlinks. + This should be connected to the Sphinx application's + "autodoc-process-docstring" event. + """ + # Your hyperlink postprocessing logic here + from docutils import nodes + import pathlib + for node in doctree.traverse(nodes.reference): + if 'refuri' in node.attributes: + refuri = node.attributes['refuri'] + if '.rst' in refuri: + if 'source' in node.document: + fpath = pathlib.Path(node.document['source']) + parent_dpath = fpath.parent + if (parent_dpath / refuri).exists(): + node.attributes['refuri'] = refuri.replace('.rst', '.html') + else: + raise AssertionError + + +def fix_rst_todo_section(lines): + new_lines = [] + for line in lines: + ... + ... def setup(app): + import sphinx + app : sphinx.application.Sphinx = app app.add_domain(PatchedPythonDomain, override=True) - if 1: - # New Way - # what = None - app.connect('autodoc-process-docstring', process) - else: - # OLD WAY - # https://stackoverflow.com/questions/26534184/can-sphinx-ignore-certain-tags-in-python-docstrings - # Register a sphinx.ext.autodoc.between listener to ignore everything - # between lines that contain the word IGNORE - # from sphinx.ext.autodoc import between - # app.connect('autodoc-process-docstring', between('^ *Ignore:$', exclude=True)) - pass + + app.connect("doctree-resolved", postprocess_hyperlinks) + + docstring_processor = GoogleStyleDocstringProcessor() + # https://stackoverflow.com/questions/26534184/can-sphinx-ignore-certain-tags-in-python-docstrings + app.connect('autodoc-process-docstring', docstring_processor.process_docstring_callback) + + def copy(src, dst): + import shutil + print(f'Copy {src} -> {dst}') + assert src.exists() + if not dst.parent.exists(): + dst.parent.mkdir() + shutil.copy(src, dst) + + ### Hack for kwcoco: TODO: figure out a way for the user to configure this. + HACK_FOR_KWCOCO = 0 + if HACK_FOR_KWCOCO: + import pathlib + doc_outdir = pathlib.Path(app.outdir) / 'auto' + doc_srcdir = pathlib.Path(app.srcdir) / 'auto' + + mod_dpath = doc_srcdir / '../../../kwcoco' + + src_fpath = (mod_dpath / 'coco_schema.json') + copy(src_fpath, doc_outdir / src_fpath.name) + copy(src_fpath, doc_srcdir / src_fpath.name) + + src_fpath = (mod_dpath / 'coco_schema_informal.rst') + copy(src_fpath, doc_outdir / src_fpath.name) + copy(src_fpath, doc_srcdir / src_fpath.name) return app diff --git a/guitool_ibeis/PrefWidget2.py b/guitool_ibeis/PrefWidget2.py index 080d182..50ed858 100644 --- a/guitool_ibeis/PrefWidget2.py +++ b/guitool_ibeis/PrefWidget2.py @@ -756,7 +756,8 @@ def _set_to_external(self, cfg): def iter_children(self): if self.children is None: - raise StopIteration() + return None + # raise StopIteration() for child in self.children: yield child @@ -980,7 +981,7 @@ def init_layout(self): def init_mvc(self): import operator - from six.moves import reduce + from functools import reduce edit_triggers = reduce(operator.__or__, [ QtWidgets.QAbstractItemView.CurrentChanged, QtWidgets.QAbstractItemView.DoubleClicked, diff --git a/guitool_ibeis/__init__.py b/guitool_ibeis/__init__.py index 65000ca..0990060 100755 --- a/guitool_ibeis/__init__.py +++ b/guitool_ibeis/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.1.2' +__version__ = '2.2.0' import utool as ut ut.noinject(__name__, '[guitool_ibeis.__init__]') diff --git a/guitool_ibeis/api_item_view.py b/guitool_ibeis/api_item_view.py index a06da9a..a5f7db3 100755 --- a/guitool_ibeis/api_item_view.py +++ b/guitool_ibeis/api_item_view.py @@ -13,10 +13,10 @@ from guitool_ibeis import api_thumb_delegate from guitool_ibeis import guitool_main from guitool_ibeis import guitool_misc -from six.moves import range, reduce # NOQA import utool import utool as ut import operator +from functools import reduce # Valid API Models from guitool_ibeis.stripe_proxy_model import StripeProxyModel from guitool_ibeis.filter_proxy_model import FilterProxyModel @@ -396,7 +396,7 @@ def itemDelegate(view, qindex): def setModel(view, model): """ QtOverride: Returns item delegate for this index """ - assert isinstance(model, VALID_API_MODELS),\ + assert isinstance(model, VALID_API_MODELS), \ ('APIItemViews only accepts APIItemModels (or one of its proxys),' 'received a %r' % type(model)) # Learn some things about the model before you fully connect it. diff --git a/guitool_ibeis/api_item_widget.py b/guitool_ibeis/api_item_widget.py index 4b632af..4c76483 100755 --- a/guitool_ibeis/api_item_widget.py +++ b/guitool_ibeis/api_item_widget.py @@ -8,9 +8,7 @@ from guitool_ibeis.api_item_model import APIItemModel from guitool_ibeis.api_table_view import APITableView from guitool_ibeis.api_tree_view import APITreeView -#from guitool_ibeis import guitool_components as comp from functools import partial -from six.moves import range import utool as ut import six (print, rrr, profile) = ut.inject2(__name__, '[APIItemWidget]') diff --git a/guitool_ibeis/api_tree_node.py b/guitool_ibeis/api_tree_node.py index 13fa399..3117c59 100755 --- a/guitool_ibeis/api_tree_node.py +++ b/guitool_ibeis/api_tree_node.py @@ -1,8 +1,7 @@ # TODO: Rename api_item_model from __future__ import absolute_import, division, print_function -from guitool_ibeis.__PYQT__ import QtCore # NOQA +from guitool_ibeis.__PYQT__ import QtCore from types import GeneratorType -from six.moves import zip, range import utool import utool as ut (print, print_, rrr) = utool.inject2(__name__) diff --git a/guitool_ibeis/guitool_components.py b/guitool_ibeis/guitool_components.py index df18b43..0032f5c 100755 --- a/guitool_ibeis/guitool_components.py +++ b/guitool_ibeis/guitool_components.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function import six -from six.moves import map, range # NOQA from guitool_ibeis.__PYQT__ import QtCore, QtGui from guitool_ibeis.__PYQT__ import QtWidgets from guitool_ibeis.__PYQT__.QtCore import Qt diff --git a/guitool_ibeis/guitool_dialogs.py b/guitool_ibeis/guitool_dialogs.py index 0be1824..6ca8630 100755 --- a/guitool_ibeis/guitool_dialogs.py +++ b/guitool_ibeis/guitool_dialogs.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, division, print_function -from six.moves import map from guitool_ibeis.__PYQT__ import QtCore, QtGui # NOQA from guitool_ibeis.__PYQT__ import QtWidgets # NOQA from guitool_ibeis.__PYQT__.QtCore import Qt diff --git a/guitool_ibeis/guitool_misc.py b/guitool_ibeis/guitool_misc.py index 1a02cf5..eddbcb8 100755 --- a/guitool_ibeis/guitool_misc.py +++ b/guitool_ibeis/guitool_misc.py @@ -5,7 +5,6 @@ import utool import sys import logging -from six.moves import range from guitool_ibeis.guitool_decorators import slot_ from guitool_ibeis import guitool_main import utool as ut diff --git a/guitool_ibeis/guitool_tables.py b/guitool_ibeis/guitool_tables.py index 144f99b..1fab121 100755 --- a/guitool_ibeis/guitool_tables.py +++ b/guitool_ibeis/guitool_tables.py @@ -5,7 +5,6 @@ from guitool_ibeis.__PYQT__.QtCore import Qt from guitool_ibeis.guitool_delegates import ComboDelegate, ButtonDelegate from guitool_ibeis import qtype -from six.moves import range, map import utool (print, rrr, profile) = utool.inject2(__name__) diff --git a/publish.sh b/publish.sh index cee2385..237ee0d 100755 --- a/publish.sh +++ b/publish.sh @@ -1,6 +1,6 @@ -#!/bin/bash -__doc__=''' -Script to publish a new version of this library on PyPI. +#!/usr/bin/env bash +__doc__=' +Script to publish a new version of this library on PyPI. If your script has binary dependencies then we assume that you have built a proper binary wheel with auditwheel and it exists in the wheelhouse directory. @@ -12,35 +12,39 @@ signing, but nothing will be uploaded to pypi unless the user explicitly sets DO_UPLOAD=True or answers yes to the prompts. Args: - TWINE_USERNAME (str) : + TWINE_USERNAME (str) : username for pypi. This must be set if uploading to pypi. Defaults to "". - TWINE_PASSWORD (str) : + TWINE_PASSWORD (str) : password for pypi. This must be set if uploading to pypi. Defaults to "". - DO_GPG (bool) : + DO_GPG (bool) : If True, sign the packages with a GPG key specified by `GPG_KEYID`. defaults to auto. - DO_UPLOAD (bool) : + DO_OTS (bool) : + If True, make an opentimestamp for the package and signature (if + available) + + DO_UPLOAD (bool) : If True, upload the packages to the pypi server specified by `TWINE_REPOSITORY_URL`. - DO_BUILD (bool) : + DO_BUILD (bool) : If True, will execute the setup.py build script, which is expected to use setuptools. In the future we may add support for other build systems. If False, this script will expect the pre-built packages to exist in "wheelhouse/{NAME}-{VERSION}-{SUFFIX}.{EXT}". - Defaults to "auto". + Defaults to "auto". - DO_TAG (bool) : - if True, will "git tag" the current HEAD with + DO_TAG (bool) : + if True, will "git tag" the current HEAD with - TWINE_REPOSITORY_URL (url) : - The URL of the pypi server to upload to. + TWINE_REPOSITORY_URL (url) : + The URL of the pypi server to upload to. Defaults to "auto", which if on the release branch, this will default to the live pypi server `https://upload.pypi.org/legacy` otherwise this will default to the test.pypi server: @@ -50,11 +54,11 @@ Args: The keyid of the gpg key to sign with. (if DO_GPG=True). Defaults to the local git config user.signingkey - DEPLOY_REMOTE (str) : + DEPLOY_REMOTE (str) : The git remote to push any tags to. Defaults to "origin" - GPG_EXECUTABLE (path) : - Path to the GPG executable. + GPG_EXECUTABLE (path) : + Path to the GPG executable. Defaults to "auto", which chooses "gpg2" if it exists, otherwise "gpg". MODE (str): @@ -84,8 +88,8 @@ Usage: # Set your variables or load your secrets export TWINE_USERNAME= export TWINE_PASSWORD= - TWINE_REPOSITORY_URL="https://test.pypi.org/legacy/" -''' + TWINE_REPOSITORY_URL="https://test.pypi.org/legacy/" +' DEBUG=${DEBUG:=''} if [[ "${DEBUG}" != "" ]]; then @@ -111,9 +115,9 @@ check_variable(){ normalize_boolean(){ ARG=$1 ARG=$(echo "$ARG" | awk '{print tolower($0)}') - if [ "$ARG" = "true" ] || [ "$ARG" = "1" ] || [ "$ARG" = "yes" ] || [ "$ARG" = "on" ]; then + if [ "$ARG" = "true" ] || [ "$ARG" = "1" ] || [ "$ARG" = "yes" ] || [ "$ARG" = "y" ] || [ "$ARG" = "on" ]; then echo "True" - elif [ "$ARG" = "false" ] || [ "$ARG" = "0" ] || [ "$ARG" = "no" ] || [ "$ARG" = "off" ]; then + elif [ "$ARG" = "false" ] || [ "$ARG" = "0" ] || [ "$ARG" = "no" ] || [ "$ARG" = "n" ] || [ "$ARG" = "off" ]; then echo "False" else echo "$ARG" @@ -138,11 +142,21 @@ DO_UPLOAD=${DO_UPLOAD:=$ARG_1} DO_TAG=${DO_TAG:=$ARG_1} DO_GPG=${DO_GPG:="auto"} -# Verify that we want to build if [ "$DO_GPG" == "auto" ]; then DO_GPG="True" fi +DO_OTS=${DO_OTS:="auto"} +if [ "$DO_OTS" == "auto" ]; then + # Do opentimestamp if it is available + # python -m pip install opentimestamps-client + if type ots ; then + DO_OTS="True" + else + DO_OTS="False" + fi +fi + DO_BUILD=${DO_BUILD:="auto"} # Verify that we want to build if [ "$DO_BUILD" == "auto" ]; then @@ -150,6 +164,7 @@ if [ "$DO_BUILD" == "auto" ]; then fi DO_GPG=$(normalize_boolean "$DO_GPG") +DO_OTS=$(normalize_boolean "$DO_OTS") DO_BUILD=$(normalize_boolean "$DO_BUILD") DO_UPLOAD=$(normalize_boolean "$DO_UPLOAD") DO_TAG=$(normalize_boolean "$DO_TAG") @@ -162,7 +177,7 @@ DEFAULT_LIVE_TWINE_REPO_URL="https://upload.pypi.org/legacy/" TWINE_REPOSITORY_URL=${TWINE_REPOSITORY_URL:="auto"} if [[ "${TWINE_REPOSITORY_URL}" == "auto" ]]; then - #if [[ "$(cat .git/HEAD)" != "ref: refs/heads/release" ]]; then + #if [[ "$(cat .git/HEAD)" != "ref: refs/heads/release" ]]; then # # If we are not on release, then default to the test pypi upload repo # TWINE_REPOSITORY_URL=${TWINE_REPOSITORY_URL:="https://test.pypi.org/legacy/"} #else @@ -237,6 +252,7 @@ GPG_KEYID = '$GPG_KEYID' DO_UPLOAD=${DO_UPLOAD} DO_TAG=${DO_TAG} DO_GPG=${DO_GPG} +DO_OTS=${DO_OTS} DO_BUILD=${DO_BUILD} MODE_LIST_STR=${MODE_LIST_STR} " @@ -244,10 +260,10 @@ MODE_LIST_STR=${MODE_LIST_STR} # Verify that we want to tag if [[ "$DO_TAG" == "True" ]]; then - echo "About to tag VERSION='$VERSION'" + echo "About to tag VERSION='$VERSION'" else if [[ "$DO_TAG" == "False" ]]; then - echo "We are NOT about to tag VERSION='$VERSION'" + echo "We are NOT about to tag VERSION='$VERSION'" else # shellcheck disable=SC2162 read -p "Do you want to git tag and push version='$VERSION'? (input 'yes' to confirm)" ANS @@ -282,10 +298,10 @@ fi # Verify that we want to publish if [[ "$DO_UPLOAD" == "True" ]]; then - echo "About to directly publish VERSION='$VERSION'" + echo "About to directly publish VERSION='$VERSION'" else if [[ "$DO_UPLOAD" == "False" ]]; then - echo "We are NOT about to directly publish VERSION='$VERSION'" + echo "We are NOT about to directly publish VERSION='$VERSION'" else # shellcheck disable=SC2162 read -p "Are you ready to directly publish version='$VERSION'? ('yes' will twine upload)" ANS @@ -375,7 +391,7 @@ ls_array(){ } -WHEEL_PATHS=() +WHEEL_FPATHS=() for _MODE in "${MODE_LIST[@]}" do if [[ "$_MODE" == "sdist" ]]; then @@ -388,40 +404,40 @@ do echo "ERROR: bad mode" exit 1 fi - # hacky CONCAT because for some reason ls_array will return + # hacky CONCAT because for some reason ls_array will return # something that looks empty but has one empty element for new_item in "${_NEW_WHEEL_PATHS[@]}" do if [[ "$new_item" != "" ]]; then - WHEEL_PATHS+=("$new_item") + WHEEL_FPATHS+=("$new_item") fi done done # Dedup the paths -readarray -t WHEEL_PATHS < <(printf '%s\n' "${WHEEL_PATHS[@]}" | sort -u) +readarray -t WHEEL_FPATHS < <(printf '%s\n' "${WHEEL_FPATHS[@]}" | sort -u) -WHEEL_PATHS_STR=$(printf '"%s" ' "${WHEEL_PATHS[@]}") +WHEEL_PATHS_STR=$(printf '"%s" ' "${WHEEL_FPATHS[@]}") echo "WHEEL_PATHS_STR = $WHEEL_PATHS_STR" echo " MODE=$MODE VERSION='$VERSION' -WHEEL_PATHS='$WHEEL_PATHS_STR' +WHEEL_FPATHS='$WHEEL_PATHS_STR' " - +WHEEL_SIGNATURE_FPATHS=() if [ "$DO_GPG" == "True" ]; then echo " === === " - for WHEEL_PATH in "${WHEEL_PATHS[@]}" + for WHEEL_FPATH in "${WHEEL_FPATHS[@]}" do - echo "WHEEL_PATH = $WHEEL_PATH" - check_variable WHEEL_PATH + echo "WHEEL_FPATH = $WHEEL_FPATH" + check_variable WHEEL_FPATH # https://stackoverflow.com/questions/45188811/how-to-gpg-sign-a-file-that-is-built-by-travis-ci # secure gpg --export-secret-keys > all.gpg @@ -432,13 +448,15 @@ if [ "$DO_GPG" == "True" ]; then echo "Signing wheels" GPG_SIGN_CMD="$GPG_EXECUTABLE --batch --yes --detach-sign --armor --local-user $GPG_KEYID" echo "GPG_SIGN_CMD = $GPG_SIGN_CMD" - $GPG_SIGN_CMD --output "$WHEEL_PATH".asc "$WHEEL_PATH" + $GPG_SIGN_CMD --output "$WHEEL_FPATH".asc "$WHEEL_FPATH" echo "Checking wheels" - twine check "$WHEEL_PATH".asc "$WHEEL_PATH" || { echo 'could not check wheels' ; exit 1; } + twine check "$WHEEL_FPATH".asc "$WHEEL_FPATH" || { echo 'could not check wheels' ; exit 1; } echo "Verifying wheels" - $GPG_EXECUTABLE --verify "$WHEEL_PATH".asc "$WHEEL_PATH" || { echo 'could not verify wheels' ; exit 1; } + $GPG_EXECUTABLE --verify "$WHEEL_FPATH".asc "$WHEEL_FPATH" || { echo 'could not verify wheels' ; exit 1; } + + WHEEL_SIGNATURE_FPATHS+=("$WHEEL_FPATH".asc) done echo " === === @@ -448,14 +466,35 @@ else fi + +if [ "$DO_OTS" == "True" ]; then + + echo " + === === + " + if [ "$DO_GPG" == "True" ]; then + # Stamp the wheels and the signatures + ots stamp "${WHEEL_FPATHS[@]}" "${WHEEL_SIGNATURE_FPATHS[@]}" + else + # Stamp only the wheels + ots stamp "${WHEEL_FPATHS[@]}" + fi + echo " + === === + " +else + echo "DO_OTS=False, Skipping OTS sign" +fi + + if [[ "$DO_TAG" == "True" ]]; then TAG_NAME="v${VERSION}" # if we messed up we can delete the tag # git push origin :refs/tags/$TAG_NAME # and then tag with -f - # + # git tag "$TAG_NAME" -m "tarball tag $VERSION" - git push --tags $DEPLOY_REMOTE + git push --tags "$DEPLOY_REMOTE" echo "Should also do a: git push $DEPLOY_REMOTE main:release" echo "For github should draft a new release: https://github.com/PyUtils/line_profiler/releases/new" else @@ -467,17 +506,11 @@ if [[ "$DO_UPLOAD" == "True" ]]; then check_variable TWINE_USERNAME check_variable TWINE_PASSWORD "hide" - for WHEEL_PATH in "${WHEEL_PATHS[@]}" + for WHEEL_FPATH in "${WHEEL_FPATHS[@]}" do - if [ "$DO_GPG" == "True" ]; then - twine upload --username "$TWINE_USERNAME" --password=$TWINE_PASSWORD \ - --repository-url "$TWINE_REPOSITORY_URL" \ - --sign "$WHEEL_PATH".asc "$WHEEL_PATH" --skip-existing --verbose || { echo 'failed to twine upload' ; exit 1; } - else - twine upload --username "$TWINE_USERNAME" --password=$TWINE_PASSWORD \ - --repository-url "$TWINE_REPOSITORY_URL" \ - "$WHEEL_PATH" --skip-existing --verbose || { echo 'failed to twine upload' ; exit 1; } - fi + twine upload --username "$TWINE_USERNAME" "--password=$TWINE_PASSWORD" \ + --repository-url "$TWINE_REPOSITORY_URL" \ + "$WHEEL_FPATH" --skip-existing --verbose || { echo 'failed to twine upload' ; exit 1; } done echo """ !!! FINISH: LIVE RUN !!! @@ -488,7 +521,7 @@ else DEPLOY_REMOTE = '$DEPLOY_REMOTE' DO_UPLOAD = '$DO_UPLOAD' - WHEEL_PATH = '$WHEEL_PATH' + WHEEL_FPATH = '$WHEEL_FPATH' WHEEL_PATHS_STR = '$WHEEL_PATHS_STR' MODE_LIST_STR = '$MODE_LIST_STR' @@ -502,3 +535,39 @@ else !!! FINISH: DRY RUN !!! """ fi + +__devel__=' +# Checking to see how easy it is to upload packages to gitlab. +# This logic should go in the CI script, not sure if it belongs here. + + +export HOST=https://gitlab.kitware.com +export GROUP_NAME=computer-vision +export PROJECT_NAME=geowatch +PROJECT_VERSION=$(geowatch --version) +echo "$PROJECT_VERSION" + +load_secrets +export PRIVATE_GITLAB_TOKEN=$(git_token_for "$HOST") +TMP_DIR=$(mktemp -d -t ci-XXXXXXXXXX) + +curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups" > "$TMP_DIR/all_group_info" +GROUP_ID=$(cat "$TMP_DIR/all_group_info" | jq ". | map(select(.name==\"$GROUP_NAME\")) | .[0].id") +echo "GROUP_ID = $GROUP_ID" + +curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" "$HOST/api/v4/groups/$GROUP_ID" > "$TMP_DIR/group_info" +PROJ_ID=$(cat "$TMP_DIR/group_info" | jq ".projects | map(select(.name==\"$PROJECT_NAME\")) | .[0].id") +echo "PROJ_ID = $PROJ_ID" + +ls_array DIST_FPATHS "dist/*" + +for FPATH in "${DIST_FPATHS[@]}" +do + FNAME=$(basename $FPATH) + echo $FNAME + curl --header "PRIVATE-TOKEN: $PRIVATE_GITLAB_TOKEN" \ + --upload-file $FPATH \ + "https://gitlab.kitware.com/api/v4/projects/$PROJ_ID/packages/generic/$PROJECT_NAME/$PROJECT_VERSION/$FNAME" +done + +' diff --git a/pyproject.toml b/pyproject.toml index ab8276f..4412732 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,17 +15,22 @@ version = "{mod_dpath}/__init__.py::__version__" author_email="erotemic@gmail.com" license = "Apache 2" dev_status = "beta" -min_python = 3.7 +min_python = 3.8 # ci_cpython_versions = ["3.7", "3.8", "3.9", "3.10"] -supported_python_versions = ["3.7", "3.8", "3.9", "3.10"] +#supported_python_versions = ["3.7", "3.8", "3.9", "3.10"] ci_pypy_versions = [] autostage = true os = [ "linux" ] [tool.pytest.ini_options] -addopts = "-p no:doctest --xdoctest --xdoctest-style=google --ignore-glob=setup.py" -norecursedirs = ".git ignore build __pycache__ dev _skbuild" -filterwarnings = [ "default", "ignore:.*No cfgstr given in Cacher constructor or call.*:Warning", "ignore:.*Define the __nice__ method for.*:Warning", "ignore:.*private pytest class or function.*:Warning",] +addopts = "-p no:doctest --xdoctest --xdoctest-style=google --ignore-glob=setup.py --ignore-glob=docs" +norecursedirs = ".git ignore build __pycache__ dev _skbuild docs" +filterwarnings = [ + "default", + "ignore:.*No cfgstr given in Cacher constructor or call.*:Warning", + "ignore:.*Define the __nice__ method for.*:Warning", + "ignore:.*private pytest class or function.*:Warning", +] [tool.coverage.run] branch = true diff --git a/requirements.txt b/requirements.txt index bc18a50..7de5aca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -r requirements/runtime.txt -r requirements/tests.txt -r requirements/optional.txt +-r requirements/build.txt \ No newline at end of file diff --git a/requirements/graphics.txt b/requirements/graphics.txt index c80d5b7..b918a95 100644 --- a/requirements/graphics.txt +++ b/requirements/graphics.txt @@ -1,4 +1,5 @@ -# python ~/local/tools/supported_python_versions_pip.py opencv-python +# xdev availpkg opencv-python-headless +# --prefer-binary opencv-python>=4.5.5.64 ; python_version < '4.0' and python_version >= '3.11' # Python 3.11+ opencv-python>=4.5.4.58 ; python_version < '3.11' and python_version >= '3.10' # Python 3.10 opencv-python>=3.4.15.55 ; python_version < '3.10' and python_version >= '3.9' # Python 3.9 diff --git a/requirements/headless.txt b/requirements/headless.txt index a34a8b2..5aeaa00 100644 --- a/requirements/headless.txt +++ b/requirements/headless.txt @@ -1,4 +1,4 @@ -# python ~/local/tools/supported_python_versions_pip.py opencv-python-headless +# xdev availpkg opencv-python-headless # --prefer-binary opencv-python-headless>=4.5.5.64 ; python_version < '4.0' and python_version >= '3.11' # Python 3.11+ opencv-python-headless>=4.5.4.58 ; python_version < '3.11' and python_version >= '3.10' # Python 3.10 diff --git a/requirements/runtime.txt b/requirements/runtime.txt index 383ad89..19b419c 100644 --- a/requirements/runtime.txt +++ b/requirements/runtime.txt @@ -1,8 +1,11 @@ -six >= 1.10.0 -ubelt >= 1.2.2 -utool >= 2.1.6 +six>=1.16.0 ; python_version < '4.0' and python_version >= '3.12' # Python 3.12+ +six>=1.10.0 ; python_version < '3.12' # Python 3.11- -matplotlib>=3.5.0 ; python_version < '4.0' and python_version >= '3.11' # Python 3.11+ +ubelt >= 1.3.4 +utool >= 2.2.0 + +matplotlib>=3.8.4 ; python_version < '4.0' and python_version >= '3.12' # Python 3.12 +matplotlib>=3.5.0 ; python_version < '3.12' and python_version >= '3.11' # Python 3.11 matplotlib>=3.5.0 ; python_version < '3.11' and python_version >= '3.10' # Python 3.10 matplotlib>=3.3.3 ; python_version < '3.10' and python_version >= '3.9' # Python 3.9 matplotlib>=3.6.0 ; python_version < '3.9' and python_version >= '3.8' # Python 3.8 @@ -13,7 +16,8 @@ matplotlib>=1.5.3 ; python_version < '3.7' and python_version >= '3.6' vtool_ibeis >= 2.2.0 #pyflann_ibeis >= 2.2.0 -pyqt5>=5.15.5;python_version>'2.7' # +pyqt5>=5.15.10 ; python_version < '4.0' and python_version >= '3.12' # Python 3.12 +pyqt5>=5.15.5 ; python_version < '3.12' and python_version >= '2.7' # Python 3.11- cachetools>=5.0.0 ; python_version < '4.0' and python_version >= '3.11' # Python 3.11+ cachetools>=5.0.0 ; python_version < '3.11' and python_version >= '3.10' # Python 3.10 diff --git a/requirements/tests.txt b/requirements/tests.txt index c7121be..77d3d2c 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,34 +1,36 @@ -xdoctest>=0.14.0 +xdoctest >= 1.1.3 # Pin maximum pytest versions for older python versions # TODO: determine what the actual minimum and maximum acceptable versions of # pytest (that are also compatible with xdoctest) are for each legacy python # major.minor version. # See ~/local/tools/supported_python_versions_pip.py for helper script -pytest>=6.2.5 ; python_version >= '3.10.0' # Python 3.10+ -pytest>=4.6.0 ; python_version < '3.10.0' and python_version >= '3.7.0' # Python 3.7-3.9 -pytest>=4.6.0 ; python_version < '3.7.0' and python_version >= '3.6.0' # Python 3.6 +pytest>=8.1.1 ; python_version < '4.0' and python_version >= '3.13' # Python 3.13+ +pytest>=8.1.1 ; python_version < '3.13' and python_version >= '3.12' # Python 3.12 +pytest>=8.1.1 ; python_version < '3.12' and python_version >= '3.11' # Python 3.11 +pytest>=8.1.1 ; python_version < '3.11' and python_version >= '3.10' # Python 3.10 +pytest>=8.1.1 ; python_version < '3.10' and python_version >= '3.9' # Python 3.9 +pytest>=8.1.1 ; python_version < '3.9' and python_version >= '3.8' # Python 3.8 +pytest>=4.6.0 ; python_version < '3.8' and python_version >= '3.7' # Python 3.7 pytest>=4.6.0, <= 6.1.2 ; python_version < '3.6.0' and python_version >= '3.5.0' # Python 3.5 pytest>=4.6.0, <= 4.6.11 ; python_version < '3.5.0' and python_version >= '3.4.0' # Python 3.4 pytest>=4.6.0, <= 4.6.11 ; python_version < '2.8.0' and python_version >= '2.7.0' # Python 2.7 -## -#pytest-cov >= 2.6.0 ; python_version >= '3.7.0' -#pytest-cov >= 2.6.0, <= 2.8.1 ; python_version < '3.7.0' and python_version >= '3.6.0' - pytest-cov>=3.0.0 ; python_version >= '3.6.0' # Python 3.6+ pytest-cov>=2.9.0 ; python_version < '3.6.0' and python_version >= '3.5.0' # Python 3.5 pytest-cov>=2.8.1 ; python_version < '3.5.0' and python_version >= '3.4.0' # Python 3.4 pytest-cov>=2.8.1 ; python_version < '2.8.0' and python_version >= '2.7.0' # Python 2.7 # python ~/local/tools/supported_python_versions_pip.py pytest-timeout -# python ~/local/tools/supported_python_versions_pip.py codecov -pytest-timeout>=1.4.2 +pytest_timeout>=2.3.1 ; python_version < '4.0' and python_version >= '3.12' # Python 3.13+ +pytest_timeout>=1.4.2 ; python_version < '3.12' # Python 3.11- # python ~/local/tools/supported_python_versions_pip.py xdoctest # python ~/local/tools/supported_python_versions_pip.py coverage -coverage>=6.1.1 ; python_version >= '3.10' # Python 3.10+ -coverage>=5.3.1 ; python_version < '3.10' and python_version >= '3.9' # Python 3.9 +coverage>=7.3.0 ; python_version < '4.0' and python_version >= '3.12' # Python 3.12+ +coverage>=7.0.0 ; python_version < '3.12' and python_version >= '3.11' # Python 3.11 +coverage>=6.1.1 ; python_version < '3.11' and python_version >= '3.10' # Python 3.10 +coverage>=5.3.1 ; python_version < '3.10' and python_version >= '3.9' # Python 3.9 coverage>=6.1.1 ; python_version < '3.9' and python_version >= '3.8' # Python 3.8 coverage>=6.1.1 ; python_version < '3.8' and python_version >= '3.7' # Python 3.7 coverage>=6.1.1 ; python_version < '3.7' and python_version >= '3.6' # Python 3.6 @@ -36,5 +38,3 @@ coverage>=5.3.1 ; python_version < '3.6' and python_version >= '3.5' # Py coverage>=4.3.4 ; python_version < '3.5' and python_version >= '3.4' # Python 3.4 coverage>=5.3.1 ; python_version < '3.4' and python_version >= '2.7' # Python 2.7 coverage>=4.5 ; python_version < '2.7' and python_version >= '2.6' # Python 2.6 - -codecov>=2.0.15 diff --git a/setup.py b/setup.py index 49fe4a8..002ece0 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,8 @@ # Generated by ~/code/xcookie/xcookie/builders/setup.py # based on part ~/code/xcookie/xcookie/rc/setup.py.in import sys -from os.path import exists +import re +from os.path import exists, dirname, join from setuptools import find_packages from setuptools import setup @@ -53,8 +54,6 @@ def parse_description(): pandoc --from=markdown --to=rst --output=README.rst README.md python -c "import setup; print(setup.parse_description())" """ - from os.path import dirname, join, exists - readme_fpath = join(dirname(__file__), "README.rst") # This breaks on pip install, so check that it exists. if exists(readme_fpath): @@ -77,10 +76,10 @@ def parse_requirements(fname="requirements.txt", versions=False): Returns: List[str]: list of requirements items - """ - from os.path import exists, dirname, join - import re + CommandLine: + python -c "import setup, ubelt; print(ubelt.urepr(setup.parse_requirements()))" + """ require_fpath = fname def parse_line(line, dpath=""): @@ -198,35 +197,40 @@ def gen_packages_items(): NAME = "guitool_ibeis" INIT_PATH = "guitool_ibeis/__init__.py" -VERSION = parse_version("guitool_ibeis/__init__.py") +VERSION = parse_version(INIT_PATH) if __name__ == "__main__": setupkw = {} - setupkw["install_requires"] = parse_requirements("requirements/runtime.txt") + setupkw["install_requires"] = parse_requirements( + "requirements/runtime.txt", versions="loose" + ) setupkw["extras_require"] = { - "all": parse_requirements("requirements.txt"), - "tests": parse_requirements("requirements/tests.txt"), - "optional": parse_requirements("requirements/optional.txt"), - "headless": parse_requirements("requirements/headless.txt"), - "graphics": parse_requirements("requirements/graphics.txt"), - # Strict versions + "all": parse_requirements("requirements.txt", versions="loose"), + "headless": parse_requirements("requirements/headless.txt", versions="loose"), + "graphics": parse_requirements("requirements/graphics.txt", versions="loose"), + "build": parse_requirements("requirements/build.txt", versions="loose"), + "docs": parse_requirements("requirements/docs.txt", versions="loose"), + "optional": parse_requirements("requirements/optional.txt", versions="loose"), + "runtime": parse_requirements("requirements/runtime.txt", versions="loose"), + "tests": parse_requirements("requirements/tests.txt", versions="loose"), + "all-strict": parse_requirements("requirements.txt", versions="strict"), "headless-strict": parse_requirements( "requirements/headless.txt", versions="strict" ), "graphics-strict": parse_requirements( "requirements/graphics.txt", versions="strict" ), - "all-strict": parse_requirements("requirements.txt", versions="strict"), + "build-strict": parse_requirements("requirements/build.txt", versions="strict"), + "docs-strict": parse_requirements("requirements/docs.txt", versions="strict"), + "optional-strict": parse_requirements( + "requirements/optional.txt", versions="strict" + ), "runtime-strict": parse_requirements( "requirements/runtime.txt", versions="strict" ), "tests-strict": parse_requirements("requirements/tests.txt", versions="strict"), - "optional-strict": parse_requirements( - "requirements/optional.txt", versions="strict" - ), } - setupkw["name"] = NAME setupkw["version"] = VERSION setupkw["author"] = "Jon Crall" @@ -237,16 +241,17 @@ def gen_packages_items(): setupkw["long_description_content_type"] = "text/x-rst" setupkw["license"] = "Apache 2" setupkw["packages"] = find_packages(".") - setupkw["python_requires"] = ">=3.7" + setupkw["python_requires"] = ">=3.8" setupkw["classifiers"] = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Utilities", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] setup(**setupkw)