bump version to 3.6.4 + CHANGES.txt (P1 of v3.6.4 release) #787
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # This workflow will install Python dependencies, run tests and lint with a single version of Python | |
| # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions | |
| name: Testing | |
| on: | |
| push: | |
| branches: | |
| - master | |
| - development | |
| pull_request: | |
| branches: | |
| - master | |
| - development | |
| # Cancel any in-progress or queued Testing run on the same source | |
| # branch when a new push arrives. Collapses duplicate push + PR runs | |
| # from the same commit into a single active run, so rapid iteration | |
| # does not stack up stale jobs. | |
| # | |
| # The group key uses github.head_ref (populated on PRs with the | |
| # source branch) falling back to github.ref_name (populated on pushes | |
| # with the branch short name), so a push to development and a PR | |
| # with development as the source end up in the same group. The | |
| # pull_request.head.repo.full_name fallback namespaces the group by | |
| # fork identity, so two different forks with the same branch name | |
| # do not collide. | |
| # | |
| # testing-cron.yml intentionally does not use concurrency because | |
| # each scheduled cron trigger is an independent run. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }} | |
| cancel-in-progress: true | |
| jobs: | |
| build: | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest] | |
| python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] | |
| # macOS is pinned to a single Python version because Apple Silicon | |
| # runners were intermittently not finishing the non-torch test suite | |
| # within the job timeout on 3.10-3.13 even though 3.9 passes | |
| # reliably. Ubuntu and Windows cover the full Python matrix, so the | |
| # single macOS cell is a smoke-test canary for Apple Silicon rather | |
| # than full per-version coverage. | |
| # TODO: re-enable 3.10-3.13 on macOS once the Apple Silicon slowness | |
| # is understood (profile via pytest --durations=20 first). | |
| include: | |
| - os: macos-latest | |
| python-version: "3.9" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install libomp for macOS | |
| if: startsWith(matrix.os, 'macos') | |
| run: brew install libomp | |
| - name: Install dependencies (non-macOS, full deps including torch) | |
| if: ${{ !startsWith(matrix.os, 'macos') }} | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r docs/requirements.txt | |
| pip install pytest | |
| pip install coverage | |
| pip install coveralls | |
| # macOS CI deliberately skips torch and torch_geometric because of | |
| # the upstream PyTorch NNPACK slowdown on Apple Silicon | |
| # (https://github.com/pytorch/pytorch/issues/107534). Conv-heavy | |
| # torch tests run ~36x slower on Apple Silicon and exhaust any | |
| # reasonable job timeout. Torch-dependent tests are auto-skipped | |
| # by pyod/test/conftest.py when torch is absent. | |
| # TODO: remove this macOS-specific step and the conftest shim once | |
| # upstream PyTorch fixes NNPACK on ARM and a fixed wheel is released. | |
| - name: Install dependencies (macOS, skip torch and torch_geometric) | |
| if: startsWith(matrix.os, 'macos') | |
| run: | | |
| python -m pip install --upgrade pip | |
| grep -vE '^(torch|torch_geometric)$' docs/requirements.txt > /tmp/mac-reqs.txt | |
| pip install -r /tmp/mac-reqs.txt | |
| pip install pytest | |
| pip install coverage | |
| pip install coveralls | |
| - name: Test with pytest | |
| run: | | |
| coverage run --source=pyod -m pytest | |
| - name: Coverage report | |
| shell: bash | |
| continue-on-error: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| COVERALLS_SERVICE_NAME: github-actions | |
| run: | | |
| if ! coveralls --service=github; then | |
| echo "Coveralls upload failed; retrying once in 15s for best effort." | |
| sleep 15 | |
| coveralls --service=github || true | |
| fi | |
| packaging-smoke-test: | |
| name: packaging smoke test (sdist + wheel from a clean build) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.11 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install build tooling | |
| run: python -m pip install --upgrade pip build twine | |
| - name: Build sdist and wheel | |
| run: python -m build | |
| - name: Inspect sdist contents | |
| run: | | |
| set -euo pipefail | |
| sdist=$(ls dist/*.tar.gz | head -1) | |
| echo "Inspecting $sdist" | |
| tar tzf "$sdist" > /tmp/sdist-listing.txt | |
| knowledge_count=$(grep -c 'pyod/utils/knowledge/.*\.json' /tmp/sdist-listing.txt || true) | |
| analysis_count=$(grep -c 'pyod/utils/model_analysis_jsons/.*\.json' /tmp/sdist-listing.txt || true) | |
| echo "knowledge JSONs in sdist: $knowledge_count" | |
| echo "model_analysis JSONs in sdist: $analysis_count" | |
| test "$knowledge_count" -ge 4 || { echo "ERROR: expected >=4 knowledge JSONs in sdist"; exit 1; } | |
| test "$analysis_count" -ge 10 || { echo "ERROR: expected >=10 model_analysis JSONs in sdist"; exit 1; } | |
| - name: Inspect wheel contents | |
| run: | | |
| set -euo pipefail | |
| wheel=$(ls dist/*.whl | head -1) | |
| echo "Inspecting $wheel" | |
| python -m zipfile -l "$wheel" > /tmp/wheel-listing.txt | |
| knowledge_count=$(grep -c 'pyod/utils/knowledge/.*\.json' /tmp/wheel-listing.txt || true) | |
| analysis_count=$(grep -c 'pyod/utils/model_analysis_jsons/.*\.json' /tmp/wheel-listing.txt || true) | |
| skill_count=$(grep -c 'pyod/skills/od_expert/SKILL\.md' /tmp/wheel-listing.txt || true) | |
| test_count=$(grep -c 'pyod/test/' /tmp/wheel-listing.txt || true) | |
| docs_count=$(grep -cE '^docs/' /tmp/wheel-listing.txt || true) | |
| echo "knowledge JSONs in wheel: $knowledge_count" | |
| echo "model_analysis JSONs in wheel: $analysis_count" | |
| echo "od_expert SKILL.md in wheel: $skill_count" | |
| echo "test files in wheel: $test_count" | |
| echo "docs files in wheel: $docs_count" | |
| test "$knowledge_count" -ge 4 || { echo "ERROR: expected >=4 knowledge JSONs in wheel"; exit 1; } | |
| test "$analysis_count" -ge 10 || { echo "ERROR: expected >=10 model_analysis JSONs in wheel"; exit 1; } | |
| test "$skill_count" -eq 1 || { echo "ERROR: expected exactly 1 od_expert SKILL.md in wheel"; exit 1; } | |
| test "$test_count" -eq 0 || { echo "ERROR: wheel contains $test_count pyod/test files; should be 0"; exit 1; } | |
| test "$docs_count" -eq 0 || { echo "ERROR: wheel contains $docs_count docs/ files; should be 0"; exit 1; } | |
| - name: Validate metadata with twine | |
| run: twine check dist/* | |
| - name: Fresh-venv install from sdist and import smoke test | |
| run: | | |
| set -euo pipefail | |
| python -m venv /tmp/pyod-smoke | |
| /tmp/pyod-smoke/bin/python -m pip install --upgrade pip | |
| /tmp/pyod-smoke/bin/python -m pip install dist/*.tar.gz | |
| /tmp/pyod-smoke/bin/python - <<'PY' | |
| import pyod | |
| from pyod.models.iforest import IForest | |
| from pyod.utils.ad_engine import ADEngine | |
| print("pyod version:", pyod.__version__) | |
| assert pyod.__version__.startswith("3."), f"unexpected version {pyod.__version__}" | |
| engine = ADEngine() | |
| detectors = engine.list_detectors() | |
| assert len(detectors) > 0, "ADEngine knowledge base loaded 0 detectors" | |
| print("ADEngine detectors loaded:", len(detectors)) | |
| import numpy as np | |
| rng = np.random.default_rng(42) | |
| X = rng.standard_normal((50, 5)) | |
| clf = IForest(contamination=0.1, random_state=42) | |
| clf.fit(X) | |
| print("IForest fit OK; decision_scores_ shape:", clf.decision_scores_.shape) | |
| PY | |
| - name: Verify unified pyod CLI end-to-end | |
| run: | | |
| set -euo pipefail | |
| /tmp/pyod-smoke/bin/pyod --help | |
| /tmp/pyod-smoke/bin/pyod info | |
| /tmp/pyod-smoke/bin/pyod install skill --help | |
| /tmp/pyod-smoke/bin/pyod install skill --list | |
| rm -rf /tmp/pyod-cli-skill-test | |
| /tmp/pyod-smoke/bin/pyod install skill --target /tmp/pyod-cli-skill-test | |
| test -f /tmp/pyod-cli-skill-test/od-expert/SKILL.md \ | |
| || { echo "ERROR: pyod install skill did not create SKILL.md"; exit 1; } | |
| # v3.2.0: verify references/ subdir was copied alongside SKILL.md | |
| test -d /tmp/pyod-cli-skill-test/od-expert/references \ | |
| || { echo "ERROR: references/ subdir not installed"; exit 1; } | |
| test -f /tmp/pyod-cli-skill-test/od-expert/references/workflow.md \ | |
| || { echo "ERROR: references/workflow.md not installed"; exit 1; } | |
| test -f /tmp/pyod-cli-skill-test/od-expert/references/pitfalls.md \ | |
| || { echo "ERROR: references/pitfalls.md not installed"; exit 1; } | |
| test -f /tmp/pyod-cli-skill-test/od-expert/references/tabular.md \ | |
| || { echo "ERROR: references/tabular.md not installed"; exit 1; } | |
| test -f /tmp/pyod-cli-skill-test/od-expert/references/time_series.md \ | |
| || { echo "ERROR: references/time_series.md not installed"; exit 1; } | |
| test -f /tmp/pyod-cli-skill-test/od-expert/references/graph.md \ | |
| || { echo "ERROR: references/graph.md not installed"; exit 1; } | |
| test -f /tmp/pyod-cli-skill-test/od-expert/references/text_image.md \ | |
| || { echo "ERROR: references/text_image.md not installed"; exit 1; } | |
| # v3.2.0: __pycache__ and __init__.py must NOT be installed | |
| test ! -e /tmp/pyod-cli-skill-test/od-expert/__pycache__ \ | |
| || { echo "ERROR: __pycache__ leaked into install"; exit 1; } | |
| test ! -e /tmp/pyod-cli-skill-test/od-expert/__init__.py \ | |
| || { echo "ERROR: __init__.py leaked into install"; exit 1; } | |
| # Regression guard: `import pyod.mcp_server` in a smoke venv | |
| # that deliberately does NOT have pyod[mcp] must not sys.exit. | |
| /tmp/pyod-smoke/bin/python -c "import importlib.util; assert importlib.util.find_spec('mcp') is None, 'smoke venv unexpectedly has mcp'" | |
| /tmp/pyod-smoke/bin/python -c "import pyod.mcp_server" | |
| # Canonical-name assertion: underscore input → hyphen output | |
| rm -rf /tmp/pyod-cli-underscore-test | |
| /tmp/pyod-smoke/bin/pyod install skill --skill od_expert \ | |
| --target /tmp/pyod-cli-underscore-test \ | |
| | tee /tmp/pyod-cli-underscore.log | |
| grep -q "Installed od-expert skill" /tmp/pyod-cli-underscore.log \ | |
| || { echo "ERROR: canonical name not used in output"; exit 1; } | |
| test -f /tmp/pyod-cli-underscore-test/od-expert/SKILL.md | |
| echo "unified pyod CLI works end-to-end" |