CI #16
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
| name: CI | |
| on: | |
| push: | |
| branches: [ master, main, develop, '**' ] | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| - '.gitignore' | |
| - 'LICENSE' | |
| pull_request: | |
| branches: [ master, main, develop ] | |
| paths-ignore: | |
| - '**.md' | |
| - 'docs/**' | |
| - '.gitignore' | |
| - 'LICENSE' | |
| schedule: | |
| # Run tests once a week to catch dependency issues | |
| - cron: '0 0 * * 0' | |
| jobs: | |
| test: | |
| name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] | |
| include: | |
| # Add Python 3.13 only for Ubuntu where it's more stable | |
| - os: ubuntu-latest | |
| python-version: '3.13-dev' | |
| # Set timeout for the entire job | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| allow-prereleases: true | |
| - name: Display Python version | |
| run: python -c "import sys; print(sys.version)" | |
| - name: Cache pip packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/pip | |
| ~/Library/Caches/pip | |
| ~/AppData/Local/pip/Cache | |
| key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements*.txt') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip-${{ matrix.python-version }}- | |
| ${{ runner.os }}-pip- | |
| - name: Upgrade pip | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install --upgrade setuptools wheel | |
| - name: Install test dependencies | |
| run: | | |
| pip install pytest pytest-cov pytest-xdist pytest-timeout parameterized | |
| - name: Install package dependencies | |
| run: | | |
| # Install all requirements first | |
| pip install ipython>=7.0.0 | |
| pip install matplotlib>=3.0.0 numpy>=1.20.0 pandas>=1.3.0 | |
| pip install flask>=2.0.0 pytz>=2019.3 scikit-learn>=0.22.0 scipy>=1.5.0 seaborn>=0.10.0 | |
| - name: Install empyrical (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| # Try installing empyrical with retries | |
| for i in 1 2 3; do | |
| echo "Attempt $i to install empyrical..." | |
| if pip install git+https://github.com/cloudQuant/empyrical.git; then | |
| echo "Successfully installed empyrical from GitHub" | |
| break | |
| elif [ $i -eq 3 ]; then | |
| echo "GitHub failed, trying Gitee..." | |
| pip install git+https://gitee.com/yunjinqi/empyrical.git || exit 1 | |
| else | |
| echo "Attempt $i failed, retrying..." | |
| sleep 5 | |
| fi | |
| done | |
| - name: Install empyrical (Windows) | |
| if: runner.os == 'Windows' | |
| shell: cmd | |
| run: | | |
| pip install git+https://github.com/cloudQuant/empyrical.git || pip install git+https://gitee.com/yunjinqi/empyrical.git | |
| - name: Verify dependencies installed | |
| run: | | |
| echo "=== Verifying critical dependencies ===" | |
| pip show ipython || (echo "IPython not installed!" && exit 1) | |
| pip show parameterized || (echo "parameterized not installed!" && exit 1) | |
| pip show empyrical || (echo "empyrical not installed!" && exit 1) | |
| - name: Install pyfolio | |
| run: | | |
| pip install -e . --no-deps | |
| # Verify installation | |
| python -c "import pyfolio; print(f'PyFolio {pyfolio.__version__} installed successfully')" | |
| - name: Run tests with pytest | |
| run: | | |
| # First verify imports work | |
| python -c "import pyfolio; print(f'PyFolio {pyfolio.__version__} OK')" | |
| python -c "import IPython; print(f'IPython {IPython.__version__} OK')" | |
| python -c "import parameterized; print('parameterized OK')" | |
| # Run tests | |
| pytest tests/ -v --cov=pyfolio --cov-report=xml --cov-report=term -n auto --tb=short | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results-${{ matrix.os }}-${{ matrix.python-version }} | |
| path: | | |
| coverage.xml | |
| .coverage | |
| - name: Upload coverage to Codecov | |
| if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| file: ./coverage.xml | |
| flags: unittests | |
| name: codecov-umbrella | |
| fail_ci_if_error: false | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install flake8 black isort mypy | |
| pip install ipython>=7.0.0 | |
| # Install empyrical for linting | |
| pip install git+https://github.com/cloudQuant/empyrical.git || pip install git+https://gitee.com/yunjinqi/empyrical.git | |
| pip install -e . | |
| - name: Lint with flake8 | |
| run: | | |
| # Stop the build if there are Python syntax errors or undefined names | |
| flake8 pyfolio --count --select=E9,F63,F7,F82 --show-source --statistics | |
| # exit-zero treats all errors as warnings | |
| flake8 pyfolio --count --exit-zero --max-complexity=10 --max-line-length=100 --statistics | |
| - name: Check formatting with black | |
| run: | | |
| black --check pyfolio || echo "Code formatting issues found. Run 'black pyfolio' to fix." | |
| - name: Check import sorting with isort | |
| run: | | |
| isort --check-only pyfolio || echo "Import sorting issues found. Run 'isort pyfolio' to fix." | |
| build: | |
| name: Build distribution | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install build | |
| - name: Build distribution | |
| run: python -m build | |
| - name: Check distribution | |
| run: | | |
| pip install twine | |
| twine check dist/* | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dist | |
| path: dist/ | |
| test-install: | |
| name: Test installation | |
| needs: build | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| continue-on-error: true # Don't fail the entire workflow if this fails | |
| strategy: | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| python-version: ['3.8', '3.11'] | |
| exclude: | |
| # Python 3.8 has issues on macOS ARM64 runners | |
| - os: macos-latest | |
| python-version: '3.8' | |
| include: | |
| # Use Python 3.9 for macOS instead | |
| - os: macos-latest | |
| python-version: '3.9' | |
| steps: | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Download artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Install from wheel | |
| shell: bash | |
| run: | | |
| set -e # Exit on error | |
| # Show system info for debugging | |
| echo "=== System Information ===" | |
| echo "OS: ${{ runner.os }}" | |
| echo "Python: ${{ matrix.python-version }}" | |
| python --version | |
| pip --version | |
| echo "=========================" | |
| # Upgrade pip for better performance | |
| echo "=== Upgrading pip/wheel/setuptools ===" | |
| python -m pip install --upgrade pip wheel setuptools | |
| pip --version | |
| # Install dependencies first with timeout | |
| echo "=== Installing IPython ===" | |
| pip install --timeout=120 ipython>=7.0.0 || { | |
| echo "Failed to install IPython, retrying..." | |
| sleep 5 | |
| pip install --timeout=120 ipython>=7.0.0 | |
| } | |
| # Install empyrical with increased timeout and retry | |
| echo "=== Installing empyrical ===" | |
| for i in 1 2 3; do | |
| echo "Attempt $i to install empyrical..." | |
| if pip install --timeout=300 git+https://github.com/cloudQuant/empyrical.git; then | |
| echo "Successfully installed empyrical from GitHub" | |
| break | |
| elif [ $i -eq 3 ]; then | |
| echo "GitHub failed, trying Gitee..." | |
| pip install --timeout=300 git+https://gitee.com/yunjinqi/empyrical.git || { | |
| echo "ERROR: Failed to install empyrical from both sources" | |
| pip list | |
| exit 1 | |
| } | |
| else | |
| echo "Attempt $i failed, waiting before retry..." | |
| sleep 10 | |
| fi | |
| done | |
| # Verify empyrical installation | |
| echo "=== Verifying empyrical ===" | |
| pip show empyrical || exit 1 | |
| # List files to ensure wheel exists | |
| echo "=== Listing dist directory ===" | |
| ls -la dist/ || { | |
| echo "ERROR: dist directory not found" | |
| pwd | |
| ls -la | |
| exit 1 | |
| } | |
| # Install pyfolio wheel | |
| echo "=== Installing pyfolio wheel ===" | |
| wheel_file=$(ls dist/*.whl | head -n 1) | |
| if [ -z "$wheel_file" ]; then | |
| echo "ERROR: No wheel file found in dist/" | |
| exit 1 | |
| fi | |
| echo "Installing wheel: $wheel_file" | |
| pip install --timeout=120 "$wheel_file" || { | |
| echo "ERROR: Failed to install pyfolio wheel" | |
| pip list | |
| exit 1 | |
| } | |
| # Show installed packages | |
| echo "=== Installed packages ===" | |
| pip list | grep -E "(pyfolio|empyrical|ipython|matplotlib|pandas|numpy)" | |
| # Verify installation | |
| echo "=== Verifying pyfolio installation ===" | |
| python -c "import pyfolio; print(f'Successfully imported pyfolio {pyfolio.__version__}')" || { | |
| echo "ERROR: Failed to import pyfolio" | |
| python -c "import sys; print('Python path:', sys.path)" | |
| exit 1 | |
| } | |
| - name: Build matplotlib font cache | |
| if: runner.os == 'macOS' | |
| continue-on-error: true | |
| run: | | |
| # Pre-build matplotlib font cache to avoid timeout during tests | |
| echo "=== Building matplotlib font cache ===" | |
| python -c " | |
| import os | |
| os.environ['MPLBACKEND'] = 'Agg' | |
| try: | |
| import matplotlib.font_manager | |
| matplotlib.font_manager._rebuild() | |
| print('Font cache built successfully') | |
| except Exception as e: | |
| print(f'Warning: Failed to build font cache: {e}') | |
| print('This is not critical, continuing...') | |
| " | |
| - name: Test basic functionality | |
| timeout-minutes: 5 | |
| run: | | |
| python -c " | |
| import os | |
| # Disable matplotlib GUI backend to avoid issues | |
| os.environ['MPLBACKEND'] = 'Agg' | |
| import pyfolio | |
| import pandas as pd | |
| import numpy as np | |
| # Create sample returns | |
| dates = pd.date_range('2020-01-01', periods=252, freq='D') | |
| returns = pd.Series(np.random.randn(252) * 0.01, index=dates) | |
| # Test basic stats computation | |
| print('Testing pyfolio basic functionality...') | |
| print(f'Annual return: {pyfolio.timeseries.annual_return(returns):.2%}') | |
| print(f'Sharpe ratio: {pyfolio.timeseries.sharpe_ratio(returns):.2f}') | |
| print('Basic functionality test passed!') | |
| " | |