# Python-Specific Guidelines

This document contains Python-specific instructions for AO projects. Reference this alongside the main `copilot-instructions.md`.

## Testing Framework: pytest

### Preferred Test Structure
```python
# tests/test_feature.py
import pytest
from pathlib import Path

class TestFeatureName:
    """Group related tests in classes."""
    
    def test_happy_path(self, tmp_path: Path):
        """Test the expected successful behavior."""
        pass
    
    def test_edge_case(self, tmp_path: Path):
        """Test boundary conditions."""
        pass
    
    def test_error_handling(self):
        """Test exception paths."""
        with pytest.raises(ValueError):
            raise ValueError("expected")
```

### Test Isolation Patterns

**Use `tmp_path` fixture for file operations:**
```python
def test_save_file(tmp_path: Path):
    # tmp_path is a unique, auto-cleaned temporary directory
    test_file = tmp_path / "test.txt"
    test_file.write_text("content")
    assert test_file.read_text() == "content"
    # Cleaned up automatically after test
```

**Use `tmp_path_factory` for module/session scope:**
```python
@pytest.fixture(scope="module")
def shared_data_dir(tmp_path_factory):
    """Shared directory for all tests in this module."""
    return tmp_path_factory.mktemp("data")
```

**Use `monkeypatch` for environment variables:**
```python
def test_config(monkeypatch):
    monkeypatch.setenv("API_KEY", "test-key")
    # Test code that reads API_KEY
```

### Mocking
```python
from unittest.mock import Mock, patch, MagicMock

def test_with_mock():
    mock_service = Mock()
    mock_service.get_data.return_value = {"key": "value"}
    
@patch("module.external_function")
def test_with_patch(mock_func):
    mock_func.return_value = "mocked"
```

### Fixtures
```python
@pytest.fixture
def sample_data():
    """Provides test data, cleaned up after test."""
    data = create_sample()
    yield data
    # Cleanup runs after test
    data.cleanup()

@pytest.fixture(scope="session")
def expensive_resource():
    """Created once per test session."""
    return create_expensive_thing()
```

## Package Management: uv

### Common Commands
```bash
# Install dependencies
uv sync

# Add a dependency
uv add package-name

# Add dev dependency
uv add --dev package-name

# Run a command
uv run pytest
uv run python -m module

# Create virtual environment
uv venv

# Build package
uv build

# Publish package
uv publish
```

### pyproject.toml Structure
```toml
[project]
name = "package-name"
version = "0.1.0"
description = "Package description"
requires-python = ">=3.11"
dependencies = [
    "click>=8.0",
    "rich>=13.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "pytest-cov>=4.0",
    "mypy>=1.0",
    "ruff>=0.1",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"

[tool.ruff]
line-length = 100
target-version = "py311"

[tool.mypy]
python_version = "3.11"
strict = true
```

## Code Quality: Ruff

### Commands
```bash
# Check for issues
uv run ruff check .

# Fix auto-fixable issues
uv run ruff check . --fix

# Format code
uv run ruff format .
```

### Configuration
```toml
[tool.ruff]
line-length = 100
target-version = "py311"
select = [
    "E",    # pycodestyle errors
    "W",    # pycodestyle warnings
    "F",    # pyflakes
    "I",    # isort
    "B",    # flake8-bugbear
    "C4",   # flake8-comprehensions
    "UP",   # pyupgrade
]
ignore = ["E501"]  # Line too long (handled by formatter)
```

## Type Checking: mypy

### Commands
```bash
uv run mypy src/
uv run mypy --strict src/
```

### Common Annotations
```python
from typing import Optional, List, Dict, Callable, TypeVar
from collections.abc import Iterable, Generator

def process(items: list[str]) -> dict[str, int]:
    return {item: len(item) for item in items}

def optional_value(value: str | None = None) -> str:
    return value or "default"
```

## Build System Commands

### Build/Test Cycle
```bash
# Full validation cycle
uv run ruff check . --fix
uv run ruff format .
uv run pytest
uv run mypy src/

# Or use a build script if available
uv run python scripts/build.py
```

### Coverage
```bash
uv run pytest --cov=src --cov-report=html
# View at htmlcov/index.html
```

## Common Patterns

### Logging
```python
import logging

logger = logging.getLogger(__name__)

def process():
    logger.info("Processing started")
    logger.debug("Detailed information")
    logger.warning("Something unusual")
    logger.error("Something failed")
```

### CLI with Typer/Click
```python
import typer
from rich.console import Console

app = typer.Typer()
console = Console()

@app.command()
def main(name: str, verbose: bool = False):
    if verbose:
        console.print(f"[dim]Processing {name}[/dim]")
    console.print(f"[green]Hello, {name}![/green]")
```

### Async Code
```python
import asyncio
from typing import Any

async def fetch_data(url: str) -> dict[str, Any]:
    # async implementation
    pass

async def main():
    results = await asyncio.gather(
        fetch_data("url1"),
        fetch_data("url2"),
    )
    return results
```

## Build Pipeline

### Pipeline Stages

A comprehensive Python build pipeline runs these stages in order:

| Stage | Tool | Purpose |
|-------|------|---------|
| 1. Dependencies | `uv sync` | Install/update dependencies |
| 2. Format | `ruff format` | Auto-format code |
| 3. Lint | `ruff check` | Check code quality |
| 4. Type Check | `mypy` | Verify type annotations |
| 5. Security | `ruff check` (S rules) | Security vulnerability scan |
| 6. Unit Tests | `pytest --cov` | Run tests with coverage |
| 7. Coverage | `pytest-cov` | Enforce coverage thresholds |

### Build Script Pattern

```python
#!/usr/bin/env python3
"""build.py - Unified build script for Python projects."""
import subprocess
import sys
from pathlib import Path

def run(cmd: list[str], check: bool = True) -> bool:
    """Run a command and return success status."""
    print(f"Running: {' '.join(cmd)}")
    result = subprocess.run(cmd)
    if check and result.returncode != 0:
        return False
    return True

def main() -> int:
    steps = [
        (["uv", "sync"], "Sync dependencies"),
        (["uv", "run", "ruff", "format", "."], "Format code"),
        (["uv", "run", "ruff", "check", ".", "--fix"], "Lint code"),
        (["uv", "run", "mypy", "src/"], "Type check"),
        (["uv", "run", "pytest", "--cov=src", "--cov-fail-under=70"], "Run tests"),
    ]
    
    for cmd, name in steps:
        print(f"\n{'='*40}\n{name}\n{'='*40}")
        if not run(cmd):
            print(f"❌ {name} FAILED")
            return 1
        print(f"✅ {name} OK")
    
    print("\n🎉 BUILD SUCCESSFUL")
    return 0

if __name__ == "__main__":
    sys.exit(main())
```

### Coverage Configuration

```toml
# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = [
    "-v",
    "--tb=short",
    "--cov=src",
    "--cov-report=term-missing",
    "--cov-report=html:htmlcov",
    "--cov-report=xml:coverage.xml",
    "--cov-fail-under=70",
]

[tool.coverage.run]
source = ["src"]
omit = ["tests/*", "*/__pycache__/*"]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "if __name__ == .__main__.:",
    "raise NotImplementedError",
    "@abstractmethod",
]
```

### Security Scanning with Ruff

```toml
# pyproject.toml - Enable security rules
[tool.ruff.lint]
select = [
    "E", "W", "F", "I", "B", "C4", "UP",
    "S",    # flake8-bandit (security)
]

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]  # Allow assert in tests
```

Common security rules:
- `S101`: Use of assert (can be optimized away)
- `S105-S107`: Hardcoded passwords/secrets
- `S301-S324`: Pickle, SQL injection, etc.
- `S501-S506`: Weak cryptography

## CI/CD Integration

### GitHub Actions

```yaml
# .github/workflows/python.yml
name: Python Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.10', '3.11', '3.12']
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Install uv
      uses: astral-sh/setup-uv@v4
      with:
        enable-cache: true
    
    - name: Set up Python
      run: uv python install ${{ matrix.python-version }}
    
    - name: Install dependencies
      run: uv sync --extra dev
    
    - name: Format check
      run: uv run ruff format --check .
    
    - name: Lint
      run: uv run ruff check .
    
    - name: Type check
      run: uv run mypy src/
    
    - name: Run tests
      run: uv run pytest --cov --cov-report=xml
    
    - name: Upload coverage
      uses: codecov/codecov-action@v4
      with:
        files: ./coverage.xml
        fail_ci_if_error: true
```

### Azure DevOps

```yaml
# azure-pipelines.yml
trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

strategy:
  matrix:
    Python310:
      python.version: '3.10'
    Python311:
      python.version: '3.11'

steps:
- task: UsePythonVersion@0
  inputs:
    versionSpec: '$(python.version)'

- script: |
    curl -LsSf https://astral.sh/uv/install.sh | sh
    export PATH="$HOME/.cargo/bin:$PATH"
    uv sync --extra dev
    uv run pytest --cov --cov-report=xml --cov-report=html
  displayName: 'Run Build Pipeline'

- task: PublishCodeCoverageResults@2
  inputs:
    codeCoverageTool: 'Cobertura'
    summaryFileLocation: 'coverage.xml'
    reportDirectory: 'htmlcov'
```

## Troubleshooting

### Common Issues

#### "uv not found"

```bash
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# On Windows
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

# Verify
uv --version
```

#### "Coverage below threshold"

```bash
# See what's not covered
uv run pytest --cov=src --cov-report=term-missing

# View detailed HTML report
uv run pytest --cov=src --cov-report=html
# Open htmlcov/index.html

# Temporarily lower threshold
uv run pytest --cov-fail-under=60
```

#### "Type checking errors with mypy"

```bash
# Run with verbose output
uv run mypy src/ --show-error-codes

# Ignore specific module
# Add to pyproject.toml:
# [[tool.mypy.overrides]]
# module = "problematic_module"
# ignore_errors = true
```

#### "Import errors in tests"

```bash
# Ensure package is installed in editable mode
uv pip install -e .

# Or use src layout properly
# pyproject.toml should have:
# [tool.setuptools.packages.find]
# where = ["src"]
```

#### "Ruff and Black conflict"

Ruff includes a formatter compatible with Black. Use only Ruff:
```bash
# Remove Black, use Ruff for both
uv run ruff format .      # Format
uv run ruff check . --fix # Lint
```
