Skip to content

bump version to 3.6.4 + CHANGES.txt (P1 of v3.6.4 release) #787

bump version to 3.6.4 + CHANGES.txt (P1 of v3.6.4 release)

bump version to 3.6.4 + CHANGES.txt (P1 of v3.6.4 release) #787

Workflow file for this run

# 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"