diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..1de0bf1 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203, W503 +extend-select = B +exclude = .git,__pycache__,build,dist,.venv,.eggs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5702407..7475f27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,46 +1,146 @@ -name: Python CI +# GitHub Actions CI workflow: lint, type-check, test, notebooks, upload artifacts +# - Python matrix: 3.10, 3.11, 3.12 +# - Uses pip cache to speed up installs +# - Runs isort (check), Black (check), Flake8 (with bugbear), Mypy, Pytest (with coverage) +# - Executes notebooks (if any) using jupyter nbconvert +name: CI on: push: - branches: [ main ] + branches: + - main + - develop + - 'releases/*' pull_request: - branches: [ main ] + branches: + - main + - develop + schedule: + - cron: '0 3 * * *' + workflow_dispatch: + +env: + PIP_CACHE_DIR: ${{ runner.cache }}/pip jobs: - build: + lint-type-test: + name: Lint / Type-check / Test (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12'] + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python.version }}- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install "black==24.*" "isort==5.*" "flake8==7.*" "flake8-bugbear==23.*" "mypy==1.*" "pytest==8.*" "pytest-cov" "nbval" "jupyter" "coverage" + + - name: Run isort (check) + run: | + isort --version + isort --check-only . + + - name: Run Black (check) + run: | + black --version + black --check . + + - name: Run Flake8 (including bugbear checks) + run: | + flake8 . + + - name: Run Mypy + run: | + mypy . + - name: Run Pytest (with coverage) + continue-on-error: false + run: | + pytest --maxfail=1 --disable-warnings -q --cov=. --cov-report=xml + + - name: Upload test/coverage artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-and-tests-${{ matrix.python-version }} + path: | + coverage.xml + .pytest_cache || true + + - name: Upload coverage to Codecov (optional) + if: secrets.CODECOV_TOKEN != '' + uses: codecov/codecov-action@v4 + with: + files: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + + notebooks: + name: Execute Jupyter notebooks (single runner) + runs-on: ubuntu-latest + needs: lint-type-test + strategy: + matrix: + python-version: ['3.11'] steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Clean any existing .egg-info directories - run: | - find . -name "*.egg-info" -exec rm -rf {} + - - - name: Set up Python 3.11 - uses: actions/setup-python@v4 - with: - python-version: 3.11 - - - name: Upgrade pip, setuptools, wheel - run: | - python -m pip install --upgrade pip setuptools wheel - - - name: Install editable package with compatibility flags - run: | - pip install -e . --use-pep517 --config-settings editable_mode=compat - - - name: Install test dependencies - run: | - pip install -r requirements.txt - - - name: Run tests with pytest - run: | - PYTHONPATH=$(pwd)/src pytest tests --maxfail=1 --disable-warnings -q --cov=src - - - name: Upload coverage report - uses: actions/upload-artifact@v4 - with: - name: coverage-report - path: coverage.xml + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache pip for notebooks + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: notebooks-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + notebooks-pip-${{ matrix.python.version }}- + + - name: Install jupyter & nbconvert + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install "jupyter" "nbconvert" "nbformat" + + - name: Find and execute notebooks + run: | + set -euo pipefail + mapfile -t NOTEBOOKS < <(git ls-files '*.ipynb' || true) + if [ ${#NOTEBOOKS[@]} -eq 0 ]; then + echo "No notebooks found in repository — skipping execution." + exit 0 + fi + echo "Found notebooks:" + printf '%s\n' "${NOTEBOOKS[@]}" + for nb in "${NOTEBOOKS[@]}"; do + echo "Executing $nb" + out="/tmp/$(basename "$nb" .ipynb)-executed.ipynb" + jupyter nbconvert --to notebook --execute "$nb" --ExecutePreprocessor.timeout=600 --output "$out" + echo "$out" + done + + - name: Upload executed notebooks (artifact) + if: always() + uses: actions/upload-artifact@v4 + with: + name: executed-notebooks + path: /tmp/*-executed.ipynb diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..d6fa740 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,7 @@ +[mypy] +python_version = 3.11 +ignore_missing_imports = True +warn_unused_configs = True +disallow_untyped_defs = False +disallow_untyped_calls = False +check_untyped_defs = False diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c2dc00e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[tool.black] +line-length = 88 +target-version = ["py310"] +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +line_length = 88 +combine_as_imports = true +known_first_party = "OptionsLab"