文件预览

python-profile.md

查看 Code QC 技能包中的文件内容。

文件内容

references/python-profile.md

# Python QC Profile

## Project Detection

Python project if any of these exist:
- `pyproject.toml`
- `setup.py` or `setup.cfg`
- `requirements.txt`
- `Pipfile`
- Top-level `*.py` files

## Virtual Environment Detection

Check for active/available virtual environment in order:

```bash
# 1. Already active
echo $VIRTUAL_ENV

# 2. Common venv directories
for dir in .venv venv env .env; do
    if [ -f "$dir/bin/activate" ] || [ -f "$dir/Scripts/activate" ]; then
        echo "Found: $dir"
    fi
done

# 3. Poetry
if [ -f "poetry.lock" ]; then
    poetry env info --path 2>/dev/null
fi

# 4. Conda
if [ -f "environment.yml" ] || [ -f "environment.yaml" ]; then
    echo "Conda environment file found"
    # Check if conda env exists
    conda env list | grep -q "$(basename $PWD)"
fi

# 5. PDM
if [ -f "pdm.lock" ]; then
    pdm info --env 2>/dev/null
fi

# 6. Hatch
if [ -f "hatch.toml" ] || grep -q "[tool.hatch]" pyproject.toml 2>/dev/null; then
    hatch env find 2>/dev/null
fi

# 7. Project-specific activate script
if [ -f "activate.sh" ]; then
    echo "Found: activate.sh"
fi
```

**Activation for QC:**
```bash
# Auto-activate if found
if [ -f ".venv/bin/activate" ]; then
    source .venv/bin/activate
elif [ -f "venv/bin/activate" ]; then
    source venv/bin/activate
elif [ -f "poetry.lock" ]; then
    poetry shell || poetry run pytest
fi
```

## Test Runner Detection

Check in order:
1. `pyproject.toml` → `[tool.pytest.ini_options]` → `pytest`
2. `pytest.ini` → `pytest`
3. `setup.cfg` → `[tool:pytest]` → `pytest`
4. `tox.ini` → `tox`
5. `noxfile.py` → `nox`
6. Fallback: `python -m pytest tests/`

## Phase 1: Test Suite with Coverage

### Basic Run
```bash
pytest -v --tb=short
```

### With Coverage
```bash
# Coverage for the main package
pytest --cov=<package_name> --cov-report=term-missing --cov-report=json --cov-fail-under=0

# Parse coverage JSON for reporting
python -c "import json; d=json.load(open('coverage.json')); print(f\"Coverage: {d['totals']['percent_covered']:.1f}%\")"
```

### Scientific Computing Projects

Large test suites with GPU/slow tests often use markers:

```bash
# Skip slow tests for quick QC
pytest -m "not slow"

# Skip GPU tests on CPU-only machines
pytest -m "not gpu"

# Run only unit tests (skip integration)
pytest -m "not integration"

# Common marker combinations
pytest -m "not (slow or gpu or integration)"
```

**Detect available markers:**
```bash
pytest --markers | grep -E "^@pytest.mark\.(slow|gpu|integration|e2e)"
```

### No Tests Handling

If no tests found:
- Check for `tests/`, `test/`, `*_test.py`, `test_*.py`
- If directory exists but empty: **SKIP** with note "Test directory exists but no tests"
- If no test directory: **SKIP** with note "No test suite configured"
- Do NOT fail for missing tests (project may be library without tests)

## Phase 3: Static Analysis with ruff

Install if needed: `pip install ruff`

### Standard Check
```bash
ruff check --select E722,T201,B006,F401,F841,UP,I --statistics <project>
```

### Fix Mode
```bash
# Safe auto-fixes only
ruff check --fix --select E,F,I,UP <project>

# Also format
ruff format <project>
```

### Recommended Rule Set

| Rule | What it catches |
|------|----------------|
| `E722` | Bare `except:` without exception type |
| `T201` | `print()` statement found (should use logging) |
| `B006` | Mutable default argument (`def f(x=[])`) |
| `F401` | Unused import |
| `F841` | Unused local variable |
| `UP` | Pyupgrade: outdated syntax for target Python version |
| `I` | isort: import ordering |

### Severity Mapping

- `E722`, `B006` → WARNING (potential bugs)
- `T201` → WARNING (code hygiene)
- `F401`, `F841` → INFO (cleanup)
- Anything ruff calls ERROR → ERROR

### Strict Mode (Optional)

For stricter QC, add security and complexity checks:
```bash
ruff check --select E722,T201,B006,F401,F841,UP,I,S,C90,PT,RUF --statistics .
```

## Phase 3.5: Type Checking

### mypy
```bash
# Basic check
mypy <package> --ignore-missing-imports

# Strict mode
mypy <package> --strict --ignore-missing-imports

# With config from pyproject.toml
mypy <package>
```

### pyright (if configured)
```bash
# Check if pyright is configured
grep -q "tool.pyright" pyproject.toml && pyright <package>
```

### Type Coverage Estimate
```bash
# Count typed vs untyped function signatures
grep -rn "def " --include="*.py" | wc -l  # total
grep -rn "def .*(.*:.*) ->" --include="*.py" | wc -l  # typed
```

## Import Check

Use `scripts/import_check.py`:

```bash
python scripts/import_check.py <package> --exclude vendor1,vendor2 --json
```

Common exclusions:
- Vendored dependencies: `aot`, `sam`, `dinov2`
- Test fixtures: `fixtures`, `mocks`
- Migration scripts: `migrations`, `alembic`
- Generated code: `generated`, `proto`

## Smoke Test Patterns

### Service Layer
```python
import tempfile
from package.service import create_thing, get_thing

def smoke_test_thing_service():
    with tempfile.TemporaryDirectory() as tmp:
        result = create_thing(tmp, "test")
        assert result is not None
        fetched = get_thing(tmp, "test")
        assert fetched is not None
    return "PASS"
```

### CLI (typer/click)
```python
from typer.testing import CliRunner
from package.cli import app

def smoke_test_cli():
    runner = CliRunner()
    result = runner.invoke(app, ["--help"])
    assert result.exit_code == 0
    return "PASS"
```

### Config Round-trip
```python
from package.config import Config

def smoke_test_config():
    cfg = Config()
    d = cfg.to_dict()
    cfg2 = Config.from_dict(d)
    assert cfg.field == cfg2.field
    return "PASS"
```

### FastAPI/Flask Endpoints
```python
from fastapi.testclient import TestClient
from package.app import app

def smoke_test_api():
    client = TestClient(app)
    # Health check
    r = client.get("/health")
    assert r.status_code == 200
    # API version
    r = client.get("/api/v1/version")
    assert r.status_code == 200
    return "PASS"
```

### Numerical/ML Backward Compatibility
```python
import numpy as np

def smoke_test_model_backward_compat():
    """Verify model produces same output as baseline."""
    from package.model import predict
    
    # Fixed input for reproducibility
    np.random.seed(42)
    test_input = np.random.randn(1, 10)
    
    result = predict(test_input)
    
    # Compare to saved baseline (update when intentionally changing)
    baseline = np.load("tests/baselines/model_output.npy")
    np.testing.assert_allclose(result, baseline, rtol=1e-5)
    return "PASS"
```

### Database/ORM
```python
import tempfile
from package.db import init_db, Session
from package.models import User

def smoke_test_database():
    with tempfile.NamedTemporaryFile(suffix=".db") as f:
        init_db(f"sqlite:///{f.name}")
        with Session() as session:
            user = User(name="test")
            session.add(user)
            session.commit()
            assert session.query(User).count() == 1
    return "PASS"
```

## UI Verification

### Gradio
```python
def smoke_test_gradio_ui():
    import os
    os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
    from package.ui import create_ui
    
    demo = create_ui()
    assert demo is not None
    # Don't call launch()
    return "PASS"
```

### Streamlit
```bash
# Run in headless mode, should exit cleanly
timeout 10 streamlit run app.py --headless --server.headless true 2>&1 | grep -q "error" && echo "FAIL" || echo "PASS"
```

### PyQt/PySide
```python
import os
os.environ["QT_QPA_PLATFORM"] = "offscreen"

def smoke_test_qt_ui():
    from package.ui.main_window import MainWindow
    from PySide6.QtWidgets import QApplication
    
    app = QApplication([])
    window = MainWindow()
    assert window is not None
    # Don't show() or exec()
    return "PASS"
```

## Dependency Security

```bash
# pip-audit (recommended)
pip-audit --json

# safety (alternative)
safety check --json

# Check for known vulnerabilities
pip-audit --strict --desc on
```