diff --git a/.github/workflows/pcds.yml b/.github/workflows/pcds.yml index 5da8a59..65f0a25 100644 --- a/.github/workflows/pcds.yml +++ b/.github/workflows/pcds.yml @@ -37,34 +37,82 @@ jobs: system-packages: "" use-setuptools-scm: true - pip-test: + pip-test-pyside6: strategy: fail-fast: false matrix: include: - python-version: "3.9" - deploy-on-success: true + upload-artifact: true - python-version: "3.10" - python-version: "3.11" - python-version: "3.12" - python-version: "3.13" - name: "Pip" - uses: pcdshub/pcds-ci-helpers/.github/workflows/python-pip-test.yml@master + name: "Pip test (PySide6)" + uses: ./.github/workflows/python-pip-test.yml secrets: inherit with: + artifact-name: "PySide6" + package-name: "qtpynodeeditor" + python-version: ${{ matrix.python-version }} + experimental: ${{ matrix.experimental || false }} + deploy-on-success: ${{ matrix.deploy-on-success || false }} + system-packages: "libegl1" + testing-extras: "PySide6" + + pip-test-pyqt5: + strategy: + fail-fast: false + matrix: + include: + - python-version: "3.9" + - python-version: "3.10" + - python-version: "3.11" + - python-version: "3.12" + - python-version: "3.13" + + name: "Pip test (pyqt5)" + uses: ./.github/workflows/python-pip-test.yml + secrets: inherit + with: + artifact-name: "PyQt5" package-name: "qtpynodeeditor" python-version: ${{ matrix.python-version }} experimental: ${{ matrix.experimental || false }} deploy-on-success: ${{ matrix.deploy-on-success || false }} system-packages: "" - testing-extras: "" + testing-extras: "PyQt5" - # pip-docs: - # name: "Documentation" - # uses: pcdshub/pcds-ci-helpers/.github/workflows/python-docs.yml@master - # with: - # package-name: "qtpynodeeditor" - # python-version: "3.9" - # deploy: ${{ github.repository_owner == "klauer" && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')) }} - # system-packages: "" + # pip-test-pyqt6: + # strategy: + # fail-fast: false + # matrix: + # include: + # - python-version: "3.9" + # deploy-on-success: true + # - python-version: "3.10" + # - python-version: "3.11" + # - python-version: "3.12" + # - python-version: "3.13" + # + # name: "Pip test (pyqt6)" + # uses: ./.github/workflows/python-pip-test.yml + # secrets: inherit + # with: + # artifact-name: "PyQt6" + # package-name: "qtpynodeeditor" + # python-version: ${{ matrix.python-version }} + # experimental: ${{ matrix.experimental || false }} + # deploy-on-success: ${{ matrix.deploy-on-success || false }} + # system-packages: "" + # testing-extras: "PyQt6" + # + # pip-docs: + # name: "Documentation" + # uses: pcdshub/pcds-ci-helpers/.github/workflows/python-docs.yml@master + # with: + # package-name: "qtpynodeeditor" + # python-version: "3.9" + # deploy: ${{ github.repository_owner == "klauer" && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')) }} + # system-packages: "" diff --git a/.github/workflows/python-pip-test.yml b/.github/workflows/python-pip-test.yml new file mode 100644 index 0000000..510233b --- /dev/null +++ b/.github/workflows/python-pip-test.yml @@ -0,0 +1,233 @@ +# Borrowed from https://github.com/pcdshub/pcds-ci-helpers/blob/master/.github/workflows/python-pip-test.yml +name: Pip Build + +on: + workflow_call: + inputs: + artifact-name: + description: "Artifact name" + required: true + type: string + package-name: + description: "The Python-importable package name to be tested" + required: true + type: string + python-version: + description: "The Python version to build and test with" + required: true + type: string + experimental: + description: "Mark this version as experimental and not required to pass" + required: false + default: false + type: boolean + upload-artifact: + description: "Deploy to PyPI on success (and when appropriate)" + required: false + default: false + type: boolean + deploy-on-success: + description: "Deploy to PyPI on success (and when appropriate)" + required: false + default: false + type: boolean + testing-extras: + default: "" + description: "Extra packages to be installed for testing" + required: false + type: string + ci-extras: + default: "" + description: "CI-specific packages to be installed" + required: false + type: string + system-packages: + default: "" + description: "CI-specific system packages required for installation" + required: false + type: string + outputs: {} + +env: + MPLBACKEND: "agg" + QT_QPA_PLATFORM: "offscreen" + +jobs: + test: + name: "Python ${{ inputs.python-version }}: pip" + runs-on: ubuntu-20.04 + continue-on-error: ${{ inputs.experimental }} + + defaults: + run: + # The following allows for each run step to utilize ~/.bash_profile + # for setting up the per-step initial state. + # --login: a login shell. Source ~/.bash_profile + # -e: exit on first error + # -o pipefail: piped processes are important; fail if they fail + shell: bash --login -eo pipefail {0} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: "recursive" + + - name: Check version to be built + run: | + # NOTE: If you run CI on your own fork, you may not have the right version + # number for the package. Synchronize your tags with the upstream, + # otherwise cross-dependencies may result in confusing build failure. + (echo "Package version: $(git describe --tags)" | tee "$GITHUB_STEP_SUMMARY") || \ + echo "::warning::Git tags not found in repository. Build may fail!" + + - name: Check environment variables for issues + run: | + echo "* Package to be built: ${{ inputs.package-name }}" + echo "* Pip 'extras' for CI testing: ${{ inputs.testing-extras }}" + echo "* General pip packages required for CI testing: ${{ inputs.ci-extras }}" + + - name: Install required system packages + if: inputs.system-packages != '' + run: | + sudo apt-get update + sudo apt-get -y install ${{ inputs.system-packages }} + + - name: Prepare for log files + run: | + mkdir $HOME/logs + + - uses: actions/setup-python@v5 + with: + python-version: "${{ inputs.python-version }}" + + - name: Upgrade pip + run: | + pip install --upgrade pip + + - name: Build wheel and source distribution + run: | + python -m pip install twine build + export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) + echo "Source date epoch set to ${SOURCE_DATE_EPOCH} for reproducible build" + # See: https://github.com/python/cpython/pull/5200 + # And: https://reproducible-builds.org/docs/source-date-epoch/ + # (I learned about this from DLS) + python -m build --sdist --wheel --outdir ./dist + + - name: Check the source distribution + run: | + python -m venv test-source-env + source test-source-env/bin/activate + + python -m pip install ./dist/*.gz + if [ ! -z "${{ inputs.testing-extras }}" ]; then + python -m pip install ${{ inputs.testing-extras }} + fi + python -c "import ${{ inputs.package-name }}; print('Imported ${{ inputs.package-name }} successfully')" + + - name: Use the wheel for testing + run: | + python -m venv test-wheel-env + source test-wheel-env/bin/activate + + python -m pip install ./dist/*.whl + if [ ! -z "${{ inputs.testing-extras }}" ]; then + python -m pip install ${{ inputs.testing-extras }} + fi + python -c "import ${{ inputs.package-name }}; print('Imported ${{ inputs.package-name }} successfully')" + + - name: Installing CI extras and testing extras + run: | + # 1. escape '<' so the user doesn't have to + # 2. escape '>' so the user doesn't have to + # 3. allow conda/pip to use the same requirements spec; + # conda expects pkg=ver but pip expects pkg==ver; using a basic + # (not =<>)=(not =) to avoid incompatibility with macOS sed not supporting + # '=\+' + input_requirements=$( + echo "${{ inputs.ci-extras }} ${{ inputs.testing-extras }}" | + sed -e "s//\>/g" | + sed -e 's/\([^=<>]\)=\([^=]\)/\1==\2/g' + ) + + declare -a test_requirements=() + for req in $input_requirements; do + test_requirements+=( "$req" ) + done + + set -x + if [[ ${#test_requirements[@]} -gt 0 ]]; then + echo "CI extras: ${{ inputs.ci-extras }}" + echo "Testing extras: ${{ inputs.testing-extras }}" + pip install "${test_requirements[@]}" .[test] + else + echo "No extras to install." + pip install .[test] + fi + + - name: Check the pip packages in the test env + run: | + pip list + + - name: Run tests + run: | + pytest -v \ + --log-file="$HOME/logs/debug_log.txt" \ + --log-format='%(asctime)s.%(msecs)03d %(module)-15s %(levelname)-8s %(threadName)-10s %(message)s' \ + --log-file-date-format='%H:%M:%S' \ + --log-level=DEBUG \ + 2>&1 | tee "$HOME/logs/pytest_log.txt" + + - name: After failure + if: ${{ failure() }} + run: | + # On failure: + # * Include the pip package details + # * Include the pytest log in the step summary (but not in the step output as it's available in the previous step) + # * Include the debug log in the step output (but not the step summary as it's too verbose) + ( + echo "### Pip list" + echo "
" + echo "" + echo '```' + pip list | egrep -v -e "^#" + echo '```' + echo "
" + + echo "" + echo "### Pytest log" + echo '```python' + cat "$HOME/logs/pytest_log.txt" || echo "# Pytest log not found?" + echo '```' + ) >> "$GITHUB_STEP_SUMMARY" + + echo "## Debug log" + cat "$HOME/logs/debug_log.txt" || echo "Debug logfile not found?" + + # - name: Upload log file artifacts + # if: ${{ always() }} + # uses: actions/upload-artifact@v4 + # with: + # name: Python ${{ inputs.python-version }} - ${{ inputs.artifact-name }} - testing log + # path: "~/logs" + + - name: Upload the package as an artifact + if: ${{ inputs.upload-artifact }} + uses: actions/upload-artifact@v4 + with: + name: Python ${{ inputs.python-version }} - ${{ inputs.artifact-name }} - package + path: dist + + - name: PyPI deployment + if: inputs.deploy-on-success && github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + if [ -z "$TWINE_PASSWORD" ]; then + echo "# No PYPI_TOKEN secret in job!" | tee -a "$GITHUB_STEP_SUMMARY" + exit 1 + fi + twine upload --verbose dist/* diff --git a/MANIFEST.in b/MANIFEST.in index 64b9c69..cea9ec8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,14 +2,11 @@ include AUTHORS.rst include CONTRIBUTING.rst include LICENSE include README.rst -include requirements.txt -include dev-requirements.txt recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-include docs *.rst conf.py Makefile make.bat -include versioneer.py include qtpynodeeditor/_version.py include qtpynodeeditor/DefaultStyle.json diff --git a/README.rst b/README.rst index 07e920c..4cb69d4 100644 --- a/README.rst +++ b/README.rst @@ -53,7 +53,9 @@ qtpynodeeditor may also be installed using pip from PyPI. :: - $ python -m pip install qtpynodeeditor pyqt5 + $ python -m pip install qtpynodeeditor[pyqt5] + $ python -m pip install qtpynodeeditor[pyqt6] + $ python -m pip install qtpynodeeditor[pyside] Running the Tests @@ -63,5 +65,5 @@ Tests must be run with pytest and pytest-qt. :: - $ pip install -r dev-requirements.txt + $ pip install .[pyqt5,test] $ pytest -v qtpynodeeditor/tests diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index b346ade..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -# These are required for developing the package (running the tests, building -# the documentation) but not necessarily required for _using_ it. -pytest -pytest-qt -pytest-cov -sphinxcontrib-jquery diff --git a/docs-requirements.txt b/docs-requirements.txt deleted file mode 100644 index cb6625e..0000000 --- a/docs-requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -sphinx -ipython -matplotlib -numpydoc -sphinx-copybutton -sphinx_rtd_theme diff --git a/pyproject.toml b/pyproject.toml index cb2f1aa..00ef16f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,40 +1,52 @@ [build-system] build-backend = "setuptools.build_meta" -requires = [ "setuptools>=45", "setuptools_scm[toml]>=6.2",] +requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"] [project] -classifiers = [ "Development Status :: 2 - Pre-Alpha", "Natural Language :: English", "Programming Language :: Python :: 3",] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Natural Language :: English", + "Programming Language :: Python :: 3", +] description = "Python Qt node editor" -dynamic = [ "version", "readme", "dependencies", "optional-dependencies", "optional-dependencies",] +dependencies = ["qtpy"] +dynamic = ["version", "readme"] keywords = [] name = "qtpynodeeditor" requires-python = ">=3.9" [[project.authors]] name = "Ken Lauer" +[project.license] +file = "LICENSE" + +[project.optional-dependencies] +pyqt = ["PyQt6"] +pyqt5 = ["PyQt5"] +pyqt6 = ["PyQt6"] +pyside = ["PySide6"] +pyside6 = ["PySide6"] +test = ["pytest", "pytest-qt", "pytest-cov"] +doc = [ + "sphinx", + "ipython", + "numpydoc", + "sphinx-copybutton", + "sphinx_rtd_theme", + "sphinxcontrib-jquery", +] + [options] zip_safe = false include_package_data = true -[project.license] -file = "LICENSE" - [tool.setuptools_scm] write_to = "qtpynodeeditor/_version.py" [tool.setuptools.packages.find] -where = [ ".",] -include = [ "qtpynodeeditor*",] +where = ["."] +include = ["qtpynodeeditor*"] namespaces = false [tool.setuptools.dynamic.readme] file = "README.rst" - -[tool.setuptools.dynamic.dependencies] -file = [ "requirements.txt",] - -[tool.setuptools.dynamic.optional-dependencies.test] -file = "dev-requirements.txt" - -[tool.setuptools.dynamic.optional-dependencies.doc] -file = "docs-requirements.txt" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 58cf3ee..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -versioneer -qtpy -PyQt5