mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
924 lines
19 KiB
Markdown
924 lines
19 KiB
Markdown
# Python Coding Standards
|
|||
|
|
|
||
|
|
Comprehensive standards for Python development in the RoboCo system. These standards are enforced through automated tooling (see [Tooling Enforcement](#tooling-enforcement) section).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Table of Contents
|
||
|
|
|
||
|
|
1. [Code Style](#code-style)
|
||
|
|
2. [Type Safety](#type-safety)
|
||
|
|
3. [Error Handling](#error-handling)
|
||
|
|
4. [Data Validation](#data-validation)
|
||
|
|
5. [Async Patterns](#async-patterns)
|
||
|
|
6. [Testing](#testing)
|
||
|
|
7. [Dependencies](#dependencies)
|
||
|
|
8. [Tooling Enforcement](#tooling-enforcement)
|
||
|
|
9. [Code Complexity](#code-complexity)
|
||
|
|
10. [Security](#security)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Code Style
|
||
|
|
|
||
|
|
### PY-001: Use Type Hints
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** mypy, ruff
|
||
|
|
|
||
|
|
All function signatures MUST include type hints for parameters and return values.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good
|
||
|
|
def process_task(task_id: str, priority: int = 1) -> TaskResult:
|
||
|
|
...
|
||
|
|
|
||
|
|
async def fetch_user(user_id: UUID) -> User | None:
|
||
|
|
...
|
||
|
|
|
||
|
|
# Bad - Missing type hints
|
||
|
|
def process_task(task_id, priority=1):
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-002: Docstrings Required
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** ruff (D100-D417)
|
||
|
|
|
||
|
|
All public functions, classes, and modules MUST have docstrings following Google style.
|
||
|
|
|
||
|
|
```python
|
||
|
|
def calculate_metrics(data: list[float]) -> MetricsResult:
|
||
|
|
"""Calculate statistical metrics from data points.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
data: List of numeric values to analyze.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
MetricsResult containing mean, median, and std dev.
|
||
|
|
|
||
|
|
Raises:
|
||
|
|
ValueError: If data is empty.
|
||
|
|
"""
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-003: Import Organization
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** ruff (I001-I002)
|
||
|
|
|
||
|
|
Imports MUST be sorted in this order: stdlib, third-party, local. Use `ruff` to enforce.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good
|
||
|
|
import asyncio
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from fastapi import FastAPI
|
||
|
|
from pydantic import BaseModel
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
from roboco.models import Task, User
|
||
|
|
from roboco.services import TaskService
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-004: Line Length
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** ruff (E501)
|
||
|
|
|
||
|
|
Maximum line length is 88 characters (Black default). Use line breaks for long expressions.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good - Multi-line function call
|
||
|
|
result = await service.create_task(
|
||
|
|
title=request.title,
|
||
|
|
description=request.description,
|
||
|
|
assigned_to=agent_id,
|
||
|
|
priority=request.priority,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Good - Multi-line string
|
||
|
|
error_message = (
|
||
|
|
f"Task {task_id} cannot be claimed: "
|
||
|
|
f"current status is {task.status}, expected 'pending'"
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-005: Naming Conventions
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** ruff (N801-N818)
|
||
|
|
|
||
|
|
| Type | Convention | Example |
|
||
|
|
|------|------------|---------|
|
||
|
|
| Classes | PascalCase | `TaskService`, `UserModel` |
|
||
|
|
| Functions | snake_case | `get_user`, `process_task` |
|
||
|
|
| Variables | snake_case | `user_id`, `task_count` |
|
||
|
|
| Constants | SCREAMING_SNAKE | `MAX_RETRIES`, `DEFAULT_TIMEOUT` |
|
||
|
|
| Private | Leading underscore | `_internal_method`, `_cache` |
|
||
|
|
| Type Variables | PascalCase | `T`, `TaskT`, `ResponseT` |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Type Safety
|
||
|
|
|
||
|
|
### PY-010: Strict Mypy Configuration
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** mypy
|
||
|
|
|
||
|
|
The project uses strict mypy configuration. All code MUST pass these checks:
|
||
|
|
|
||
|
|
```toml
|
||
|
|
# pyproject.toml settings (enforced)
|
||
|
|
[tool.mypy]
|
||
|
|
python_version = "3.13"
|
||
|
|
disallow_untyped_defs = true
|
||
|
|
disallow_incomplete_defs = true
|
||
|
|
check_untyped_defs = true
|
||
|
|
no_implicit_optional = true
|
||
|
|
strict_optional = true
|
||
|
|
warn_return_any = true
|
||
|
|
warn_unreachable = true
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-011: No `Any` Types
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** mypy
|
||
|
|
|
||
|
|
Avoid `Any` type. Use `object`, generics, or `TypeVar` instead.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Bad
|
||
|
|
def process_data(data: Any) -> Any:
|
||
|
|
...
|
||
|
|
|
||
|
|
# Good - Use generics
|
||
|
|
T = TypeVar('T')
|
||
|
|
def process_data(data: T) -> T:
|
||
|
|
...
|
||
|
|
|
||
|
|
# Good - Use Union for multiple types
|
||
|
|
def process_data(data: str | bytes) -> ProcessedData:
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-012: Use `None` Explicitly
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** mypy
|
||
|
|
|
||
|
|
Use `| None` for optional values. Never use implicit optional.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Bad - Implicit optional
|
||
|
|
def get_user(user_id: str, cache: dict = None) -> User:
|
||
|
|
...
|
||
|
|
|
||
|
|
# Good - Explicit optional
|
||
|
|
def get_user(user_id: str, cache: dict[str, User] | None = None) -> User:
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-013: Use Type Aliases for Complex Types
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** ruff
|
||
|
|
|
||
|
|
Create type aliases for complex or repeated types.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good - Type aliases improve readability
|
||
|
|
type TaskCallback = Callable[[Task, TaskStatus], Awaitable[None]]
|
||
|
|
type SearchResults = list[tuple[str, float, dict[str, Any]]]
|
||
|
|
|
||
|
|
async def search_with_callback(
|
||
|
|
query: str,
|
||
|
|
callback: TaskCallback,
|
||
|
|
) -> SearchResults:
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Error Handling
|
||
|
|
|
||
|
|
### PY-020: Specific Exceptions
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** ruff (E722, B001)
|
||
|
|
|
||
|
|
NEVER use bare `except:`. Catch specific exceptions.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good
|
||
|
|
try:
|
||
|
|
result = await service.process(data)
|
||
|
|
except ValidationError as e:
|
||
|
|
logger.warning("Validation failed", error=str(e))
|
||
|
|
raise
|
||
|
|
except ServiceUnavailableError:
|
||
|
|
await retry_with_backoff(service.process, data)
|
||
|
|
|
||
|
|
# Bad - NEVER do this
|
||
|
|
try:
|
||
|
|
result = await service.process(data)
|
||
|
|
except:
|
||
|
|
pass
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-021: Use Custom Exceptions
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** code review
|
||
|
|
|
||
|
|
Define domain-specific exceptions for better error handling.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good - Custom exceptions
|
||
|
|
class TaskError(Exception):
|
||
|
|
"""Base exception for task operations."""
|
||
|
|
|
||
|
|
class TaskNotFoundError(TaskError):
|
||
|
|
"""Task does not exist."""
|
||
|
|
|
||
|
|
class TaskAlreadyClaimedError(TaskError):
|
||
|
|
"""Task is already claimed by another agent."""
|
||
|
|
|
||
|
|
# Usage
|
||
|
|
async def claim_task(task_id: str, agent_id: str) -> Task:
|
||
|
|
task = await get_task(task_id)
|
||
|
|
if task is None:
|
||
|
|
raise TaskNotFoundError(f"Task {task_id} not found")
|
||
|
|
if task.assigned_to and task.assigned_to != agent_id:
|
||
|
|
raise TaskAlreadyClaimedError(f"Task claimed by {task.assigned_to}")
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-022: Structured Logging
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** ruff, code review
|
||
|
|
|
||
|
|
Use structlog with context for all logging. NEVER use print statements.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good
|
||
|
|
import structlog
|
||
|
|
|
||
|
|
logger = structlog.get_logger(__name__)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Task completed",
|
||
|
|
task_id=task.id,
|
||
|
|
duration_ms=elapsed,
|
||
|
|
agent_id=agent.id,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Bad - Never use print
|
||
|
|
print(f"Task {task.id} completed in {elapsed}ms")
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-023: Re-raise with Context
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** code review
|
||
|
|
|
||
|
|
When catching and re-raising, preserve the original exception chain.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good - Preserve exception chain
|
||
|
|
try:
|
||
|
|
result = await external_api.call()
|
||
|
|
except ExternalAPIError as e:
|
||
|
|
raise ServiceError("External API failed") from e
|
||
|
|
|
||
|
|
# Bad - Loses original traceback
|
||
|
|
try:
|
||
|
|
result = await external_api.call()
|
||
|
|
except ExternalAPIError:
|
||
|
|
raise ServiceError("External API failed")
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Data Validation
|
||
|
|
|
||
|
|
### PY-030: Pydantic Models
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** ruff, mypy
|
||
|
|
|
||
|
|
Use Pydantic for all API request/response models and configuration.
|
||
|
|
|
||
|
|
```python
|
||
|
|
from pydantic import BaseModel, Field, field_validator
|
||
|
|
|
||
|
|
class TaskRequest(BaseModel):
|
||
|
|
title: str = Field(..., min_length=1, max_length=200)
|
||
|
|
priority: int = Field(default=1, ge=1, le=5)
|
||
|
|
tags: list[str] = Field(default_factory=list)
|
||
|
|
|
||
|
|
@field_validator('tags')
|
||
|
|
@classmethod
|
||
|
|
def validate_tags(cls, v: list[str]) -> list[str]:
|
||
|
|
return [tag.lower().strip() for tag in v]
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-031: Validate at Boundaries
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** code review
|
||
|
|
|
||
|
|
Validate external input at system boundaries. Trust internal data.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# API boundary - validate thoroughly
|
||
|
|
@router.post("/tasks")
|
||
|
|
async def create_task(request: TaskCreate) -> TaskResponse:
|
||
|
|
# Pydantic validates automatically
|
||
|
|
...
|
||
|
|
|
||
|
|
# Internal service - trust validated data
|
||
|
|
async def process_task(task: Task) -> None:
|
||
|
|
# No need to re-validate task.title here
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-032: Use Enums for Finite Values
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** ruff
|
||
|
|
|
||
|
|
Use Enums for status values, types, and other finite sets.
|
||
|
|
|
||
|
|
```python
|
||
|
|
from enum import StrEnum
|
||
|
|
|
||
|
|
class TaskStatus(StrEnum):
|
||
|
|
PENDING = "pending"
|
||
|
|
IN_PROGRESS = "in_progress"
|
||
|
|
COMPLETED = "completed"
|
||
|
|
|
||
|
|
# Usage - type-safe comparisons
|
||
|
|
if task.status == TaskStatus.PENDING:
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Async Patterns
|
||
|
|
|
||
|
|
### PY-040: Async by Default
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** code review
|
||
|
|
|
||
|
|
Use async functions for ALL I/O operations. All database and API calls MUST be async.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good - async I/O
|
||
|
|
async def fetch_user(user_id: str) -> User:
|
||
|
|
return await db.users.get(user_id)
|
||
|
|
|
||
|
|
async def call_external_api(data: dict) -> Response:
|
||
|
|
async with httpx.AsyncClient() as client:
|
||
|
|
return await client.post(url, json=data)
|
||
|
|
|
||
|
|
# Bad - Blocking I/O
|
||
|
|
def fetch_user(user_id: str) -> User:
|
||
|
|
return db.users.get(user_id) # Blocking!
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-041: Use `asyncio.gather` for Concurrent Operations
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** code review
|
||
|
|
|
||
|
|
Execute independent async operations concurrently.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good - Concurrent execution
|
||
|
|
async def get_task_details(task_id: str) -> TaskDetails:
|
||
|
|
task, comments, history = await asyncio.gather(
|
||
|
|
get_task(task_id),
|
||
|
|
get_comments(task_id),
|
||
|
|
get_history(task_id),
|
||
|
|
)
|
||
|
|
return TaskDetails(task=task, comments=comments, history=history)
|
||
|
|
|
||
|
|
# Bad - Sequential when not needed
|
||
|
|
async def get_task_details(task_id: str) -> TaskDetails:
|
||
|
|
task = await get_task(task_id)
|
||
|
|
comments = await get_comments(task_id) # Waits unnecessarily
|
||
|
|
history = await get_history(task_id) # Waits unnecessarily
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-042: Proper Context Manager Usage
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** ruff (ASYNC)
|
||
|
|
|
||
|
|
Use async context managers for resources that need cleanup.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good - Proper cleanup
|
||
|
|
async with AsyncSession(engine) as session:
|
||
|
|
async with session.begin():
|
||
|
|
result = await session.execute(query)
|
||
|
|
|
||
|
|
# Good - httpx client
|
||
|
|
async with httpx.AsyncClient() as client:
|
||
|
|
response = await client.get(url)
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-043: Avoid Blocking in Async Code
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** ruff (ASYNC), bandit
|
||
|
|
|
||
|
|
NEVER call blocking functions from async code.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Bad - Blocks event loop
|
||
|
|
async def process_file(path: Path) -> str:
|
||
|
|
return path.read_text() # Blocking!
|
||
|
|
|
||
|
|
# Good - Use async file I/O
|
||
|
|
import aiofiles
|
||
|
|
|
||
|
|
async def process_file(path: Path) -> str:
|
||
|
|
async with aiofiles.open(path) as f:
|
||
|
|
return await f.read()
|
||
|
|
|
||
|
|
# Good - Run blocking in thread pool
|
||
|
|
async def process_file(path: Path) -> str:
|
||
|
|
loop = asyncio.get_event_loop()
|
||
|
|
return await loop.run_in_executor(None, path.read_text)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
|
||
|
|
### PY-050: Test Coverage
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** pytest-cov
|
||
|
|
|
||
|
|
Maintain minimum 80% code coverage for all modules.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Run with coverage
|
||
|
|
uv run pytest --cov=roboco --cov-report=term-missing
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-051: Async Tests
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** pytest-asyncio
|
||
|
|
|
||
|
|
Use pytest-asyncio for testing async code.
|
||
|
|
|
||
|
|
```python
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_fetch_user() -> None:
|
||
|
|
user = await fetch_user("test-123")
|
||
|
|
assert user.name == "Test User"
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-052: Test Structure
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** code review
|
||
|
|
|
||
|
|
Follow AAA pattern: Arrange, Act, Assert.
|
||
|
|
|
||
|
|
```python
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_task_claim_success() -> None:
|
||
|
|
# Arrange
|
||
|
|
task = await create_test_task(status=TaskStatus.PENDING)
|
||
|
|
agent = await create_test_agent()
|
||
|
|
|
||
|
|
# Act
|
||
|
|
claimed_task = await task_service.claim(task.id, agent.id)
|
||
|
|
|
||
|
|
# Assert
|
||
|
|
assert claimed_task.status == TaskStatus.CLAIMED
|
||
|
|
assert claimed_task.assigned_to == agent.id
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-053: Use Factories for Test Data
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** code review
|
||
|
|
|
||
|
|
Use factory-boy for consistent test data generation.
|
||
|
|
|
||
|
|
```python
|
||
|
|
from factory import Factory, Faker, LazyAttribute
|
||
|
|
|
||
|
|
class TaskFactory(Factory):
|
||
|
|
class Meta:
|
||
|
|
model = Task
|
||
|
|
|
||
|
|
title = Faker('sentence')
|
||
|
|
status = TaskStatus.PENDING
|
||
|
|
created_at = LazyAttribute(lambda _: datetime.now(UTC))
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Dependencies
|
||
|
|
|
||
|
|
### PY-060: Use UV
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** pyproject.toml
|
||
|
|
|
||
|
|
Use `uv` as the package manager. Lock dependencies in `pyproject.toml` and `uv.lock`.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Add dependency
|
||
|
|
uv add package-name
|
||
|
|
|
||
|
|
# Add dev dependency
|
||
|
|
uv add --dev package-name
|
||
|
|
|
||
|
|
# Sync dependencies
|
||
|
|
uv sync
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-061: Pin Dependencies
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** deptry
|
||
|
|
|
||
|
|
Keep `uv.lock` committed. Run `uv lock` when updating dependencies.
|
||
|
|
|
||
|
|
### PY-062: Audit Dependencies
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** pip-audit, safety
|
||
|
|
|
||
|
|
Run security audits on dependencies regularly.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Audit for vulnerabilities
|
||
|
|
uv run pip-audit
|
||
|
|
uv run safety scan
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Tooling Enforcement
|
||
|
|
|
||
|
|
All Python code MUST pass these automated checks before merge.
|
||
|
|
|
||
|
|
### Ruff (Linting & Formatting)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Format code
|
||
|
|
uv run ruff format .
|
||
|
|
|
||
|
|
# Check linting
|
||
|
|
uv run ruff check .
|
||
|
|
|
||
|
|
# Auto-fix issues
|
||
|
|
uv run ruff check --fix .
|
||
|
|
```
|
||
|
|
|
||
|
|
**Enabled Rule Sets:**
|
||
|
|
|
||
|
|
| Rule | Description |
|
||
|
|
|------|-------------|
|
||
|
|
| `E`, `W` | pycodestyle (PEP 8) |
|
||
|
|
| `F` | Pyflakes (errors) |
|
||
|
|
| `I` | isort (imports) |
|
||
|
|
| `B` | flake8-bugbear (common bugs) |
|
||
|
|
| `C4` | flake8-comprehensions |
|
||
|
|
| `UP` | pyupgrade (Python upgrades) |
|
||
|
|
| `ARG` | unused arguments |
|
||
|
|
| `SIM` | simplification |
|
||
|
|
| `TCH` | type checking |
|
||
|
|
| `PTH` | pathlib usage |
|
||
|
|
| `PL` | Pylint |
|
||
|
|
| `RUF` | Ruff-specific |
|
||
|
|
|
||
|
|
### Mypy (Type Checking)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
uv run mypy roboco/
|
||
|
|
```
|
||
|
|
|
||
|
|
**Configuration (pyproject.toml):**
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[tool.mypy]
|
||
|
|
python_version = "3.13"
|
||
|
|
strict = true
|
||
|
|
plugins = ["pydantic.mypy"]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Vulture (Dead Code)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
uv run vulture roboco/ vulture_whitelist.py
|
||
|
|
```
|
||
|
|
|
||
|
|
Finds unused code. Add false positives to `vulture_whitelist.py`.
|
||
|
|
|
||
|
|
### Bandit (Security)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
uv run bandit -r roboco/ -ll
|
||
|
|
```
|
||
|
|
|
||
|
|
Scans for security issues. Severity threshold: medium.
|
||
|
|
|
||
|
|
### Radon (Complexity)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Cyclomatic complexity
|
||
|
|
uv run radon cc roboco/ -nc
|
||
|
|
|
||
|
|
# Maintainability index
|
||
|
|
uv run radon mi roboco/ -nc
|
||
|
|
```
|
||
|
|
|
||
|
|
### Xenon (Complexity Thresholds)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
uv run xenon roboco/ --max-absolute B --max-modules A --max-average A
|
||
|
|
```
|
||
|
|
|
||
|
|
**Thresholds:**
|
||
|
|
|
||
|
|
| Metric | Maximum | Grade |
|
||
|
|
|--------|---------|-------|
|
||
|
|
| Absolute complexity | B | 6-10 |
|
||
|
|
| Module complexity | A | 1-5 |
|
||
|
|
| Average complexity | A | 1-5 |
|
||
|
|
|
||
|
|
### Deptry (Dependency Analysis)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
uv run deptry .
|
||
|
|
```
|
||
|
|
|
||
|
|
Finds unused, missing, and misplaced dependencies.
|
||
|
|
|
||
|
|
### Semgrep (Static Analysis)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
uv run semgrep --config=auto roboco/
|
||
|
|
```
|
||
|
|
|
||
|
|
Advanced pattern-based static analysis.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Code Complexity
|
||
|
|
|
||
|
|
### PY-070: Maximum Cyclomatic Complexity
|
||
|
|
|
||
|
|
**Severity:** ERROR
|
||
|
|
**Tools:** radon, xenon
|
||
|
|
|
||
|
|
Functions MUST have cyclomatic complexity <= 10 (grade B or better).
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Bad - Too complex (CC > 10)
|
||
|
|
def process_request(request: Request) -> Response:
|
||
|
|
if request.type == "A":
|
||
|
|
if request.priority == 1:
|
||
|
|
if request.urgent:
|
||
|
|
... # Deep nesting = high complexity
|
||
|
|
elif request.type == "B":
|
||
|
|
...
|
||
|
|
|
||
|
|
# Good - Decomposed into smaller functions
|
||
|
|
def process_request(request: Request) -> Response:
|
||
|
|
handler = get_handler(request.type)
|
||
|
|
return handler.process(request)
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-071: Maximum Function Length
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** code review
|
||
|
|
|
||
|
|
Functions SHOULD be <= 50 lines. Consider decomposition if longer.
|
||
|
|
|
||
|
|
### PY-072: Maximum Arguments
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** ruff (PLR0913)
|
||
|
|
|
||
|
|
Functions SHOULD have <= 5 arguments. Use dataclasses or Pydantic models for more.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Bad - Too many arguments
|
||
|
|
def create_task(
|
||
|
|
title: str,
|
||
|
|
description: str,
|
||
|
|
priority: int,
|
||
|
|
due_date: datetime,
|
||
|
|
assigned_to: str,
|
||
|
|
tags: list[str],
|
||
|
|
parent_id: str | None,
|
||
|
|
) -> Task:
|
||
|
|
...
|
||
|
|
|
||
|
|
# Good - Use a model
|
||
|
|
class TaskCreate(BaseModel):
|
||
|
|
title: str
|
||
|
|
description: str
|
||
|
|
priority: int = 1
|
||
|
|
due_date: datetime | None = None
|
||
|
|
assigned_to: str | None = None
|
||
|
|
tags: list[str] = []
|
||
|
|
parent_id: str | None = None
|
||
|
|
|
||
|
|
def create_task(params: TaskCreate) -> Task:
|
||
|
|
...
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-073: Avoid Deep Nesting
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** code review
|
||
|
|
|
||
|
|
Maximum nesting depth SHOULD be 4 levels. Use early returns and guard clauses.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Bad - Deep nesting
|
||
|
|
def process(data: Data) -> Result:
|
||
|
|
if data.valid:
|
||
|
|
if data.type == "A":
|
||
|
|
if data.ready:
|
||
|
|
if data.value > 0:
|
||
|
|
return process_a(data)
|
||
|
|
return None
|
||
|
|
|
||
|
|
# Good - Guard clauses
|
||
|
|
def process(data: Data) -> Result | None:
|
||
|
|
if not data.valid:
|
||
|
|
return None
|
||
|
|
if data.type != "A":
|
||
|
|
return None
|
||
|
|
if not data.ready:
|
||
|
|
return None
|
||
|
|
if data.value <= 0:
|
||
|
|
return None
|
||
|
|
return process_a(data)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Security
|
||
|
|
|
||
|
|
### PY-080: No Hardcoded Secrets
|
||
|
|
|
||
|
|
**Severity:** BLOCKER
|
||
|
|
**Tools:** bandit (B105, B106, B107)
|
||
|
|
|
||
|
|
NEVER hardcode secrets. Use environment variables.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Bad - NEVER do this
|
||
|
|
API_KEY = "sk-abc123xyz789"
|
||
|
|
DATABASE_URL = "postgresql://user:password@host/db"
|
||
|
|
|
||
|
|
# Good - Load from environment
|
||
|
|
from pydantic_settings import BaseSettings
|
||
|
|
|
||
|
|
class Settings(BaseSettings):
|
||
|
|
api_key: str
|
||
|
|
database_url: str
|
||
|
|
|
||
|
|
model_config = {"env_prefix": "ROBOCO_"}
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-081: Use `usedforsecurity` for Non-Security Hashes
|
||
|
|
|
||
|
|
**Severity:** WARNING
|
||
|
|
**Tools:** bandit (B324)
|
||
|
|
|
||
|
|
When using hash functions for non-security purposes, add `usedforsecurity=False`.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Good - Non-security hash for ID generation
|
||
|
|
import hashlib
|
||
|
|
|
||
|
|
content_hash = hashlib.md5(
|
||
|
|
content.encode(),
|
||
|
|
usedforsecurity=False
|
||
|
|
).hexdigest()[:12]
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-082: SQL Injection Prevention
|
||
|
|
|
||
|
|
**Severity:** BLOCKER
|
||
|
|
**Tools:** bandit (B608), semgrep
|
||
|
|
|
||
|
|
NEVER construct SQL with string concatenation. Use parameterized queries.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Bad - SQL injection vulnerability
|
||
|
|
query = f"SELECT * FROM users WHERE id = '{user_id}'"
|
||
|
|
|
||
|
|
# Good - Parameterized query
|
||
|
|
result = await session.execute(
|
||
|
|
select(User).where(User.id == user_id)
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-083: Command Injection Prevention
|
||
|
|
|
||
|
|
**Severity:** BLOCKER
|
||
|
|
**Tools:** bandit (B602, B603, B604)
|
||
|
|
|
||
|
|
NEVER pass user input directly to shell commands.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Bad - Command injection vulnerability
|
||
|
|
import os
|
||
|
|
os.system(f"process_file {filename}")
|
||
|
|
|
||
|
|
# Good - Use subprocess with list
|
||
|
|
import subprocess
|
||
|
|
if not SAFE_FILENAME_PATTERN.match(filename):
|
||
|
|
raise ValidationError("Invalid filename")
|
||
|
|
subprocess.run(["process_file", filename], check=True)
|
||
|
|
```
|
||
|
|
|
||
|
|
### PY-084: No `eval` or `exec`
|
||
|
|
|
||
|
|
**Severity:** BLOCKER
|
||
|
|
**Tools:** bandit (B307)
|
||
|
|
|
||
|
|
NEVER use `eval()` or `exec()` on untrusted input.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Bad - Code injection vulnerability
|
||
|
|
result = eval(user_input)
|
||
|
|
|
||
|
|
# Good - Use ast.literal_eval for safe parsing
|
||
|
|
import ast
|
||
|
|
result = ast.literal_eval(user_input) # Only parses literals
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Quick Reference
|
||
|
|
|
||
|
|
### Before Committing
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Format
|
||
|
|
uv run ruff format .
|
||
|
|
|
||
|
|
# Lint
|
||
|
|
uv run ruff check .
|
||
|
|
|
||
|
|
# Type check
|
||
|
|
uv run mypy roboco/
|
||
|
|
|
||
|
|
# Dead code
|
||
|
|
uv run vulture roboco/ vulture_whitelist.py
|
||
|
|
|
||
|
|
# Full check (recommended)
|
||
|
|
make lint
|
||
|
|
```
|
||
|
|
|
||
|
|
### Severity Levels
|
||
|
|
|
||
|
|
| Level | Action | Blocks PR |
|
||
|
|
|-------|--------|-----------|
|
||
|
|
| BLOCKER | Must fix immediately | Yes |
|
||
|
|
| ERROR | Must fix before merge | Yes |
|
||
|
|
| WARNING | Should fix | No |
|
||
|
|
| INFO | Consider improving | No |
|
||
|
|
|
||
|
|
### Rule ID Reference
|
||
|
|
|
||
|
|
| Prefix | Category |
|
||
|
|
|--------|----------|
|
||
|
|
| PY-00X | Code style |
|
||
|
|
| PY-01X | Type safety |
|
||
|
|
| PY-02X | Error handling |
|
||
|
|
| PY-03X | Data validation |
|
||
|
|
| PY-04X | Async patterns |
|
||
|
|
| PY-05X | Testing |
|
||
|
|
| PY-06X | Dependencies |
|
||
|
|
| PY-07X | Complexity |
|
||
|
|
| PY-08X | Security |
|