| # Code Style Guide |
|
|
| > **Last Updated**: 2025-12-06 |
|
|
| This guide covers code style conventions and tooling for DeepBoner. |
|
|
| ## Quick Reference |
|
|
| ```bash |
| # Auto-format code |
| make format |
| |
| # Check linting |
| make lint |
| |
| # Type check |
| make typecheck |
| |
| # Run all checks |
| make check |
| ``` |
|
|
| ## Tooling |
|
|
| ### Ruff (Linting & Formatting) |
|
|
| Configuration in `pyproject.toml`: |
|
|
| ```toml |
| [tool.ruff] |
| line-length = 100 |
| target-version = "py311" |
| src = ["src", "tests"] |
| |
| [tool.ruff.lint] |
| select = [ |
| "E", # pycodestyle errors |
| "F", # pyflakes |
| "B", # flake8-bugbear |
| "I", # isort |
| "N", # pep8-naming |
| "UP", # pyupgrade |
| "PL", # pylint |
| "RUF", # ruff-specific |
| ] |
| ``` |
|
|
| ### MyPy (Type Checking) |
|
|
| Configuration in `pyproject.toml`: |
|
|
| ```toml |
| [tool.mypy] |
| python_version = "3.11" |
| strict = true |
| ignore_missing_imports = true |
| disallow_untyped_defs = true |
| warn_return_any = true |
| ``` |
|
|
| ### Pre-commit Hooks |
|
|
| Hooks run automatically on commit: |
|
|
| ```yaml |
| # .pre-commit-config.yaml |
| repos: |
| - repo: https://github.com/astral-sh/ruff-pre-commit |
| hooks: |
| - id: ruff |
| - id: ruff-format |
| - repo: https://github.com/pre-commit/mirrors-mypy |
| hooks: |
| - id: mypy |
| ``` |
|
|
| ## Python Style |
|
|
| ### Type Hints |
|
|
| All functions must have type annotations: |
|
|
| ```python |
| # Good |
| def search(query: str, limit: int = 10) -> list[Evidence]: |
| """Search for evidence.""" |
| pass |
| |
| # Bad |
| def search(query, limit=10): |
| pass |
| ``` |
|
|
| Use modern type hint syntax (Python 3.11+): |
|
|
| ```python |
| # Good |
| def process(items: list[str] | None) -> dict[str, int]: |
| pass |
| |
| # Avoid (old syntax) |
| from typing import List, Dict, Optional |
| def process(items: Optional[List[str]]) -> Dict[str, int]: |
| pass |
| ``` |
|
|
| ### Docstrings |
|
|
| Use Google-style docstrings for public APIs: |
|
|
| ```python |
| def search_pubmed(query: str, max_results: int = 10) -> SearchResult: |
| """Search PubMed for biomedical literature. |
| |
| Args: |
| query: The search query string. |
| max_results: Maximum number of results to return. |
| |
| Returns: |
| SearchResult containing evidence and metadata. |
| |
| Raises: |
| SearchError: If the API call fails. |
| RateLimitError: If rate limit is exceeded. |
| """ |
| pass |
| ``` |
|
|
| ### Class Documentation |
|
|
| ```python |
| class SearchHandler: |
| """Orchestrates parallel searches across multiple sources. |
| |
| This handler implements a scatter-gather pattern to query |
| multiple biomedical databases simultaneously. |
| |
| Attributes: |
| sources: List of enabled search sources. |
| timeout: Timeout for each search in seconds. |
| |
| Example: |
| handler = SearchHandler() |
| result = handler.search_all("testosterone therapy") |
| """ |
| |
| def __init__(self, sources: list[str] | None = None) -> None: |
| """Initialize the search handler. |
| |
| Args: |
| sources: Optional list of sources to enable. |
| Defaults to all sources. |
| """ |
| pass |
| ``` |
|
|
| ### Imports |
|
|
| Imports are sorted by isort (via ruff): |
|
|
| ```python |
| # Standard library |
| import asyncio |
| from datetime import datetime |
| from typing import Any |
| |
| # Third-party |
| import httpx |
| from pydantic import BaseModel |
| |
| # Local |
| from src.utils.config import settings |
| from src.utils.exceptions import SearchError |
| ``` |
|
|
| ### Line Length |
|
|
| Maximum 100 characters. Break long lines: |
|
|
| ```python |
| # Good - break at logical points |
| result = very_long_function_name( |
| first_argument=value1, |
| second_argument=value2, |
| third_argument=value3, |
| ) |
| |
| # Good - string continuation |
| message = ( |
| "This is a very long message that needs to be " |
| "split across multiple lines for readability." |
| ) |
| ``` |
|
|
| ### Naming Conventions |
|
|
| | Type | Convention | Example | |
| |------|------------|---------| |
| | Classes | PascalCase | `SearchHandler` | |
| | Functions | snake_case | `search_pubmed` | |
| | Variables | snake_case | `max_results` | |
| | Constants | UPPER_SNAKE | `MAX_ITERATIONS` | |
| | Private | leading underscore | `_internal_method` | |
| | Type vars | PascalCase | `T`, `ConfigT` | |
|
|
| ### Exceptions |
|
|
| Custom exceptions in `src/utils/exceptions.py`: |
|
|
| ```python |
| from src.utils.exceptions import SearchError |
| |
| # Raising |
| raise SearchError(f"API returned {status_code}") |
| |
| # With cause |
| try: |
| response = client.get(url) |
| except httpx.HTTPError as e: |
| raise SearchError(f"Request failed: {e}") from e |
| ``` |
|
|
| ## Pydantic Models |
|
|
| ### Model Definition |
|
|
| ```python |
| from pydantic import BaseModel, Field |
| |
| class Evidence(BaseModel): |
| """A piece of evidence from search.""" |
| |
| content: str = Field(min_length=1, description="The evidence text") |
| relevance: float = Field(ge=0.0, le=1.0, default=0.0) |
| metadata: dict[str, Any] = Field(default_factory=dict) |
| |
| model_config = {"frozen": True} # Make immutable |
| ``` |
|
|
| ### Settings |
|
|
| ```python |
| from pydantic_settings import BaseSettings |
| |
| class Settings(BaseSettings): |
| """Application settings from environment.""" |
| |
| model_config = SettingsConfigDict( |
| env_file=".env", |
| case_sensitive=False, |
| ) |
| |
| max_iterations: int = Field(default=10, ge=1, le=50) |
| ``` |
|
|
| ## Async Code |
|
|
| ### Async Functions |
|
|
| ```python |
| async def search_async(query: str) -> SearchResult: |
| """Async search implementation.""" |
| async with httpx.AsyncClient() as client: |
| response = await client.get(url) |
| return parse_response(response) |
| ``` |
|
|
| ### Concurrent Execution |
|
|
| ```python |
| async def search_all(query: str) -> list[SearchResult]: |
| """Search all sources concurrently.""" |
| tasks = [ |
| search_pubmed(query), |
| search_clinicaltrials(query), |
| search_europepmc(query), |
| ] |
| return await asyncio.gather(*tasks, return_exceptions=True) |
| ``` |
|
|
| ## Comments |
|
|
| ### When to Comment |
|
|
| ```python |
| # Good: Explain WHY, not WHAT |
| # PubMed rate limits without API key - add delay to avoid 429 |
| await asyncio.sleep(0.34) |
| |
| # Bad: Obvious comment |
| # Increment counter |
| counter += 1 |
| ``` |
|
|
| ### TODO Comments |
|
|
| ```python |
| # TODO(username): Description of what needs to be done |
| # TODO: Short-term fix, proper solution needs X |
| ``` |
|
|
| ## Ignored Rules |
|
|
| Some rules are disabled for good reasons: |
|
|
| ```toml |
| ignore = [ |
| "PLR0913", # Too many arguments (agents need many params) |
| "PLR0912", # Too many branches (complex orchestrator logic) |
| "PLR2004", # Magic values (statistical constants) |
| "PLW0603", # Global statement (singleton pattern) |
| "PLC0415", # Lazy imports for optional dependencies |
| ] |
| ``` |
|
|
| ## File Organization |
|
|
| ### Module Structure |
|
|
| ```python |
| """Module docstring explaining purpose.""" |
| |
| # Imports (sorted) |
| import ... |
| |
| # Constants |
| MAX_RESULTS = 100 |
| |
| # Type definitions |
| ResultType = dict[str, Any] |
| |
| # Classes |
| class MyClass: |
| pass |
| |
| # Functions |
| def my_function(): |
| pass |
| |
| # Module-level code (minimize) |
| if __name__ == "__main__": |
| main() |
| ``` |
|
|
| ### Package Structure |
|
|
| ``` |
| src/tools/ |
| βββ __init__.py # Public exports |
| βββ base.py # Base classes |
| βββ pubmed.py # PubMed implementation |
| βββ clinicaltrials.py |
| βββ search_handler.py |
| ``` |
|
|
| ## Code Review Checklist |
|
|
| Before submitting a PR: |
|
|
| - [ ] All functions have type hints |
| - [ ] Public APIs have docstrings |
| - [ ] `make check` passes |
| - [ ] No hardcoded credentials |
| - [ ] Error cases are handled |
| - [ ] Tests cover new code |
|
|
| --- |
|
|
| ## Related Documentation |
|
|
| - [Testing Guide](testing.md) |
| - [Contributing Guide](../../CONTRIBUTING.md) |
| - [Architecture Overview](../architecture/overview.md) |
|
|