mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 09:37:15 +00:00
Add comprehensive Conductor plugin implementing Context-Driven Development methodology with tracks, specs, and phased implementation plans. Components: - 5 commands: setup, new-track, implement, status, revert - 1 agent: conductor-validator - 3 skills: context-driven-development, track-management, workflow-patterns - 18 templates for project artifacts Documentation updates: - README.md: Updated counts (68 plugins, 100 agents, 110 skills, 76 tools) - docs/plugins.md: Added Conductor to Workflows section - docs/agents.md: Added conductor-validator agent - docs/agent-skills.md: Added Conductor skills section Also includes Prettier formatting across all project files.
567 lines
12 KiB
Markdown
567 lines
12 KiB
Markdown
# Python Style Guide
|
|
|
|
Python conventions following PEP 8 and modern best practices.
|
|
|
|
## PEP 8 Fundamentals
|
|
|
|
### Naming Conventions
|
|
|
|
```python
|
|
# Variables and functions: snake_case
|
|
user_name = "John"
|
|
def calculate_total(items):
|
|
pass
|
|
|
|
# Constants: SCREAMING_SNAKE_CASE
|
|
MAX_CONNECTIONS = 100
|
|
DEFAULT_TIMEOUT = 30
|
|
|
|
# Classes: PascalCase
|
|
class UserAccount:
|
|
pass
|
|
|
|
# Private: single underscore prefix
|
|
class User:
|
|
def __init__(self):
|
|
self._internal_state = {}
|
|
|
|
# Name mangling: double underscore prefix
|
|
class Base:
|
|
def __init__(self):
|
|
self.__private = "truly private"
|
|
|
|
# Module-level "private": single underscore
|
|
_module_cache = {}
|
|
```
|
|
|
|
### Indentation and Line Length
|
|
|
|
```python
|
|
# 4 spaces per indentation level
|
|
def function():
|
|
if condition:
|
|
do_something()
|
|
|
|
# Line length: 88 characters (Black) or 79 (PEP 8)
|
|
# Break long lines appropriately
|
|
result = some_function(
|
|
argument_one,
|
|
argument_two,
|
|
argument_three,
|
|
)
|
|
|
|
# Implicit line continuation in brackets
|
|
users = [
|
|
"alice",
|
|
"bob",
|
|
"charlie",
|
|
]
|
|
```
|
|
|
|
### Imports
|
|
|
|
```python
|
|
# Standard library
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional, List
|
|
|
|
# Third-party
|
|
import requests
|
|
from pydantic import BaseModel
|
|
|
|
# Local application
|
|
from myapp.models import User
|
|
from myapp.utils import format_date
|
|
|
|
# Avoid wildcard imports
|
|
# Bad: from module import *
|
|
# Good: from module import specific_item
|
|
```
|
|
|
|
## Type Hints
|
|
|
|
### Basic Type Annotations
|
|
|
|
```python
|
|
from typing import Optional, List, Dict, Tuple, Union, Any
|
|
|
|
# Variables
|
|
name: str = "John"
|
|
age: int = 30
|
|
active: bool = True
|
|
scores: List[int] = [90, 85, 92]
|
|
|
|
# Functions
|
|
def greet(name: str) -> str:
|
|
return f"Hello, {name}!"
|
|
|
|
def find_user(user_id: int) -> Optional[User]:
|
|
"""Returns User or None if not found."""
|
|
pass
|
|
|
|
def process_items(items: List[str]) -> Dict[str, int]:
|
|
"""Returns count of each item."""
|
|
pass
|
|
```
|
|
|
|
### Advanced Type Hints
|
|
|
|
```python
|
|
from typing import (
|
|
TypeVar, Generic, Protocol, Callable,
|
|
Literal, TypedDict, Final
|
|
)
|
|
|
|
# TypeVar for generics
|
|
T = TypeVar('T')
|
|
def first(items: List[T]) -> Optional[T]:
|
|
return items[0] if items else None
|
|
|
|
# Protocol for structural typing
|
|
class Renderable(Protocol):
|
|
def render(self) -> str: ...
|
|
|
|
def display(obj: Renderable) -> None:
|
|
print(obj.render())
|
|
|
|
# Literal for specific values
|
|
Status = Literal["pending", "active", "completed"]
|
|
|
|
def set_status(status: Status) -> None:
|
|
pass
|
|
|
|
# TypedDict for dictionary shapes
|
|
class UserDict(TypedDict):
|
|
id: int
|
|
name: str
|
|
email: Optional[str]
|
|
|
|
# Final for constants
|
|
MAX_SIZE: Final = 100
|
|
```
|
|
|
|
### Type Hints in Classes
|
|
|
|
```python
|
|
from dataclasses import dataclass
|
|
from typing import ClassVar, Self
|
|
|
|
@dataclass
|
|
class User:
|
|
id: int
|
|
name: str
|
|
email: str
|
|
active: bool = True
|
|
|
|
# Class variable
|
|
_instances: ClassVar[Dict[int, 'User']] = {}
|
|
|
|
def deactivate(self) -> Self:
|
|
self.active = False
|
|
return self
|
|
|
|
class Builder:
|
|
def __init__(self) -> None:
|
|
self._value: str = ""
|
|
|
|
def append(self, text: str) -> Self:
|
|
self._value += text
|
|
return self
|
|
```
|
|
|
|
## Docstrings
|
|
|
|
### Function Docstrings
|
|
|
|
```python
|
|
def calculate_discount(
|
|
price: float,
|
|
discount_percent: float,
|
|
min_price: float = 0.0
|
|
) -> float:
|
|
"""Calculate the discounted price.
|
|
|
|
Args:
|
|
price: Original price of the item.
|
|
discount_percent: Discount percentage (0-100).
|
|
min_price: Minimum price floor. Defaults to 0.0.
|
|
|
|
Returns:
|
|
The discounted price, not less than min_price.
|
|
|
|
Raises:
|
|
ValueError: If discount_percent is not between 0 and 100.
|
|
|
|
Example:
|
|
>>> calculate_discount(100.0, 20.0)
|
|
80.0
|
|
"""
|
|
if not 0 <= discount_percent <= 100:
|
|
raise ValueError("Discount must be between 0 and 100")
|
|
|
|
discounted = price * (1 - discount_percent / 100)
|
|
return max(discounted, min_price)
|
|
```
|
|
|
|
### Class Docstrings
|
|
|
|
```python
|
|
class UserService:
|
|
"""Service for managing user operations.
|
|
|
|
This service handles user CRUD operations and authentication.
|
|
It requires a database connection and optional cache.
|
|
|
|
Attributes:
|
|
db: Database connection instance.
|
|
cache: Optional cache for user lookups.
|
|
|
|
Example:
|
|
>>> service = UserService(db_connection)
|
|
>>> user = service.get_user(123)
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
db: DatabaseConnection,
|
|
cache: Optional[Cache] = None
|
|
) -> None:
|
|
"""Initialize the UserService.
|
|
|
|
Args:
|
|
db: Active database connection.
|
|
cache: Optional cache instance for performance.
|
|
"""
|
|
self.db = db
|
|
self.cache = cache
|
|
```
|
|
|
|
## Virtual Environments
|
|
|
|
### Setup Commands
|
|
|
|
```bash
|
|
# Create virtual environment
|
|
python -m venv .venv
|
|
|
|
# Activate (Unix/macOS)
|
|
source .venv/bin/activate
|
|
|
|
# Activate (Windows)
|
|
.venv\Scripts\activate
|
|
|
|
# Install dependencies
|
|
pip install -r requirements.txt
|
|
|
|
# Freeze dependencies
|
|
pip freeze > requirements.txt
|
|
```
|
|
|
|
### Modern Tools
|
|
|
|
```bash
|
|
# Using uv (recommended)
|
|
uv venv
|
|
uv pip install -r requirements.txt
|
|
|
|
# Using poetry
|
|
poetry init
|
|
poetry add requests
|
|
poetry install
|
|
|
|
# Using pipenv
|
|
pipenv install
|
|
pipenv install requests
|
|
```
|
|
|
|
### Project Structure
|
|
|
|
```
|
|
project/
|
|
├── .venv/ # Virtual environment (gitignored)
|
|
├── src/
|
|
│ └── myapp/
|
|
│ ├── __init__.py
|
|
│ ├── main.py
|
|
│ └── utils.py
|
|
├── tests/
|
|
│ ├── __init__.py
|
|
│ └── test_main.py
|
|
├── pyproject.toml # Modern project config
|
|
├── requirements.txt # Pinned dependencies
|
|
└── README.md
|
|
```
|
|
|
|
## Testing
|
|
|
|
### pytest Basics
|
|
|
|
```python
|
|
import pytest
|
|
from myapp.calculator import add, divide
|
|
|
|
def test_add_positive_numbers():
|
|
assert add(2, 3) == 5
|
|
|
|
def test_add_negative_numbers():
|
|
assert add(-1, -1) == -2
|
|
|
|
def test_divide_by_zero_raises():
|
|
with pytest.raises(ZeroDivisionError):
|
|
divide(10, 0)
|
|
|
|
# Parametrized tests
|
|
@pytest.mark.parametrize("a,b,expected", [
|
|
(1, 1, 2),
|
|
(0, 0, 0),
|
|
(-1, 1, 0),
|
|
])
|
|
def test_add_parametrized(a, b, expected):
|
|
assert add(a, b) == expected
|
|
```
|
|
|
|
### Fixtures
|
|
|
|
```python
|
|
import pytest
|
|
from myapp.database import Database
|
|
from myapp.models import User
|
|
|
|
@pytest.fixture
|
|
def db():
|
|
"""Provide a clean database for each test."""
|
|
database = Database(":memory:")
|
|
database.create_tables()
|
|
yield database
|
|
database.close()
|
|
|
|
@pytest.fixture
|
|
def sample_user(db):
|
|
"""Create a sample user in the database."""
|
|
user = User(name="Test User", email="test@example.com")
|
|
db.save(user)
|
|
return user
|
|
|
|
def test_user_creation(db, sample_user):
|
|
found = db.find_user(sample_user.id)
|
|
assert found.name == "Test User"
|
|
```
|
|
|
|
### Mocking
|
|
|
|
```python
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
import pytest
|
|
|
|
def test_api_client_with_mock():
|
|
# Create mock
|
|
mock_response = Mock()
|
|
mock_response.json.return_value = {"id": 1, "name": "Test"}
|
|
mock_response.status_code = 200
|
|
|
|
with patch('requests.get', return_value=mock_response) as mock_get:
|
|
result = fetch_user(1)
|
|
|
|
mock_get.assert_called_once_with('/users/1')
|
|
assert result['name'] == "Test"
|
|
|
|
@patch('myapp.service.external_api')
|
|
def test_with_patch_decorator(mock_api):
|
|
mock_api.get_data.return_value = {"status": "ok"}
|
|
result = process_data()
|
|
assert result["status"] == "ok"
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Exception Patterns
|
|
|
|
```python
|
|
# Define custom exceptions
|
|
class AppError(Exception):
|
|
"""Base exception for application errors."""
|
|
pass
|
|
|
|
class ValidationError(AppError):
|
|
"""Raised when validation fails."""
|
|
def __init__(self, field: str, message: str):
|
|
self.field = field
|
|
self.message = message
|
|
super().__init__(f"{field}: {message}")
|
|
|
|
class NotFoundError(AppError):
|
|
"""Raised when a resource is not found."""
|
|
def __init__(self, resource: str, identifier: Any):
|
|
self.resource = resource
|
|
self.identifier = identifier
|
|
super().__init__(f"{resource} '{identifier}' not found")
|
|
```
|
|
|
|
### Exception Handling
|
|
|
|
```python
|
|
def get_user(user_id: int) -> User:
|
|
try:
|
|
user = db.find_user(user_id)
|
|
if user is None:
|
|
raise NotFoundError("User", user_id)
|
|
return user
|
|
except DatabaseError as e:
|
|
logger.error(f"Database error: {e}")
|
|
raise AppError("Unable to fetch user") from e
|
|
|
|
# Context managers for cleanup
|
|
from contextlib import contextmanager
|
|
|
|
@contextmanager
|
|
def database_transaction(db):
|
|
try:
|
|
yield db
|
|
db.commit()
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Dataclasses
|
|
|
|
```python
|
|
from dataclasses import dataclass, field
|
|
from typing import List, Optional
|
|
from datetime import datetime
|
|
|
|
@dataclass
|
|
class User:
|
|
id: int
|
|
name: str
|
|
email: str
|
|
active: bool = True
|
|
created_at: datetime = field(default_factory=datetime.now)
|
|
tags: List[str] = field(default_factory=list)
|
|
|
|
def __post_init__(self):
|
|
self.email = self.email.lower()
|
|
|
|
@dataclass(frozen=True)
|
|
class Point:
|
|
"""Immutable point."""
|
|
x: float
|
|
y: float
|
|
|
|
def distance_to(self, other: 'Point') -> float:
|
|
return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5
|
|
```
|
|
|
|
### Context Managers
|
|
|
|
```python
|
|
from contextlib import contextmanager
|
|
from typing import Generator
|
|
|
|
@contextmanager
|
|
def timer(name: str) -> Generator[None, None, None]:
|
|
"""Time a block of code."""
|
|
import time
|
|
start = time.perf_counter()
|
|
try:
|
|
yield
|
|
finally:
|
|
elapsed = time.perf_counter() - start
|
|
print(f"{name}: {elapsed:.3f}s")
|
|
|
|
# Usage
|
|
with timer("data processing"):
|
|
process_large_dataset()
|
|
|
|
# Class-based context manager
|
|
class DatabaseConnection:
|
|
def __init__(self, connection_string: str):
|
|
self.connection_string = connection_string
|
|
self.connection = None
|
|
|
|
def __enter__(self):
|
|
self.connection = connect(self.connection_string)
|
|
return self.connection
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
if self.connection:
|
|
self.connection.close()
|
|
return False # Don't suppress exceptions
|
|
```
|
|
|
|
### Decorators
|
|
|
|
```python
|
|
from functools import wraps
|
|
from typing import Callable, TypeVar, ParamSpec
|
|
import time
|
|
|
|
P = ParamSpec('P')
|
|
R = TypeVar('R')
|
|
|
|
def retry(max_attempts: int = 3, delay: float = 1.0):
|
|
"""Retry decorator with exponential backoff."""
|
|
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
@wraps(func)
|
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
last_exception = None
|
|
for attempt in range(max_attempts):
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except Exception as e:
|
|
last_exception = e
|
|
if attempt < max_attempts - 1:
|
|
time.sleep(delay * (2 ** attempt))
|
|
raise last_exception
|
|
return wrapper
|
|
return decorator
|
|
|
|
@retry(max_attempts=3, delay=0.5)
|
|
def fetch_data(url: str) -> dict:
|
|
response = requests.get(url)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
```
|
|
|
|
## Code Quality Tools
|
|
|
|
### Ruff Configuration
|
|
|
|
```toml
|
|
# pyproject.toml
|
|
[tool.ruff]
|
|
line-length = 88
|
|
target-version = "py311"
|
|
|
|
[tool.ruff.lint]
|
|
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)
|
|
|
|
[tool.ruff.lint.isort]
|
|
known-first-party = ["myapp"]
|
|
```
|
|
|
|
### Type Checking with mypy
|
|
|
|
```toml
|
|
# pyproject.toml
|
|
[tool.mypy]
|
|
python_version = "3.11"
|
|
strict = true
|
|
warn_return_any = true
|
|
warn_unused_configs = true
|
|
ignore_missing_imports = true
|
|
```
|