1009 lines
19 KiB
Markdown
1009 lines
19 KiB
Markdown
# Python Coding Standards
|
|
|
|
## Purpose
|
|
|
|
This document establishes coding standards for all Python code in StarPunk. These standards ensure consistency, readability, and maintainability while adhering to the project philosophy: "Every line of code must justify its existence."
|
|
|
|
## Philosophy
|
|
|
|
Code standards should:
|
|
- **Enhance readability** without adding ceremony
|
|
- **Catch errors early** through consistent patterns
|
|
- **Enable automation** via tools (black, flake8, mypy)
|
|
- **Stay minimal** - no rules without clear benefit
|
|
|
|
## Base Standard
|
|
|
|
StarPunk follows **PEP 8** with specific clarifications and exceptions documented below.
|
|
|
|
**Reference**: [PEP 8 - Style Guide for Python Code](https://peps.python.org/pep-0008/)
|
|
|
|
---
|
|
|
|
## Code Formatting
|
|
|
|
### Automated Formatting with Black
|
|
|
|
**Decision**: All Python code MUST be formatted with [Black](https://black.readthedocs.io/).
|
|
|
|
**Configuration**: Use Black's defaults (88 character line length).
|
|
|
|
**Rationale**:
|
|
- Eliminates formatting debates
|
|
- Consistent across entire codebase
|
|
- Automated via pre-commit hook or CI
|
|
- Industry standard
|
|
|
|
**Usage**:
|
|
```bash
|
|
# Format all code
|
|
.venv/bin/black starpunk/ tests/
|
|
|
|
# Check formatting without changes
|
|
.venv/bin/black --check starpunk/ tests/
|
|
|
|
# Format specific file
|
|
.venv/bin/black starpunk/notes.py
|
|
```
|
|
|
|
**Configuration File** (optional `pyproject.toml`):
|
|
```toml
|
|
[tool.black]
|
|
line-length = 88
|
|
target-version = ['py311']
|
|
include = '\.pyi?$'
|
|
extend-exclude = '''
|
|
/(
|
|
\.venv
|
|
| data
|
|
)/
|
|
'''
|
|
```
|
|
|
|
---
|
|
|
|
## Code Style
|
|
|
|
### Line Length
|
|
|
|
**Standard**: 88 characters (Black default)
|
|
|
|
**Exception**: Long strings, URLs, or comments can exceed if breaking them reduces readability.
|
|
|
|
**Good**:
|
|
```python
|
|
# Fits in 88 characters, readable
|
|
def create_note(content: str, published: bool = False) -> Note:
|
|
"""Create a new note with the given content."""
|
|
...
|
|
```
|
|
|
|
**Acceptable Exception**:
|
|
```python
|
|
# URL shouldn't be broken
|
|
INDIELOGIN_ENDPOINT = "https://indielogin.com/auth?me={me}&client_id={client_id}&redirect_uri={redirect_uri}&state={state}"
|
|
```
|
|
|
|
---
|
|
|
|
### Indentation
|
|
|
|
**Standard**: 4 spaces per indentation level (PEP 8).
|
|
|
|
**Never use tabs** - Black enforces this.
|
|
|
|
**Good**:
|
|
```python
|
|
def example():
|
|
if condition:
|
|
do_something()
|
|
if nested:
|
|
do_nested()
|
|
```
|
|
|
|
---
|
|
|
|
### Blank Lines
|
|
|
|
**Rules**:
|
|
- 2 blank lines between top-level functions and classes
|
|
- 1 blank line between methods in a class
|
|
- Use blank lines sparingly within functions for logical grouping
|
|
|
|
**Good**:
|
|
```python
|
|
import os
|
|
|
|
|
|
def first_function():
|
|
"""First function."""
|
|
pass
|
|
|
|
|
|
def second_function():
|
|
"""Second function."""
|
|
pass
|
|
|
|
|
|
class MyClass:
|
|
"""Example class."""
|
|
|
|
def method_one(self):
|
|
"""First method."""
|
|
pass
|
|
|
|
def method_two(self):
|
|
"""Second method."""
|
|
pass
|
|
```
|
|
|
|
---
|
|
|
|
### Imports
|
|
|
|
**Order** (enforced by Black and flake8):
|
|
1. Standard library imports
|
|
2. Third-party imports
|
|
3. Local application imports
|
|
|
|
**Within each group**: Alphabetically sorted
|
|
|
|
**Style**: One import per line (except `from` imports)
|
|
|
|
**Good**:
|
|
```python
|
|
# Standard library
|
|
import os
|
|
import sqlite3
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
# Third-party
|
|
import httpx
|
|
import markdown
|
|
from flask import Flask, render_template, request
|
|
from feedgen.feed import FeedGenerator
|
|
|
|
# Local
|
|
from starpunk.config import load_config
|
|
from starpunk.database import get_db
|
|
from starpunk.models import Note
|
|
```
|
|
|
|
**Bad**:
|
|
```python
|
|
import os, sys # Multiple imports on one line
|
|
from starpunk.config import * # Star imports
|
|
from flask import Flask
|
|
import httpx # Wrong order (third-party before local)
|
|
from starpunk.models import Note
|
|
```
|
|
|
|
**Absolute vs Relative Imports**:
|
|
- Use absolute imports: `from starpunk.database import get_db`
|
|
- Avoid relative imports: `from .database import get_db`
|
|
|
|
---
|
|
|
|
### Quotes
|
|
|
|
**Standard**: Use double quotes `"` for strings (Black default).
|
|
|
|
**Exception**: Use single quotes to avoid escaping doubles inside string.
|
|
|
|
**Good**:
|
|
```python
|
|
name = "StarPunk"
|
|
description = "A minimal CMS"
|
|
html = '<a href="/note">Link</a>' # Single quotes to avoid escaping
|
|
```
|
|
|
|
**Bad**:
|
|
```python
|
|
name = 'StarPunk' # Black will change to double quotes
|
|
```
|
|
|
|
---
|
|
|
|
## Naming Conventions
|
|
|
|
### Files and Modules
|
|
|
|
**Pattern**: `lowercase_with_underscores.py`
|
|
|
|
**Good**:
|
|
```
|
|
starpunk/database.py
|
|
starpunk/notes.py
|
|
starpunk/utils.py
|
|
tests/test_auth.py
|
|
```
|
|
|
|
**Bad**:
|
|
```
|
|
starpunk/Database.py # CamelCase
|
|
starpunk/db.py # Abbreviation
|
|
starpunk/note-utils.py # Hyphens
|
|
```
|
|
|
|
---
|
|
|
|
### Classes
|
|
|
|
**Pattern**: `PascalCase` (CapWords)
|
|
|
|
**Good**:
|
|
```python
|
|
class Note:
|
|
pass
|
|
|
|
class SessionManager:
|
|
pass
|
|
|
|
class MicropubEndpoint:
|
|
pass
|
|
```
|
|
|
|
**Bad**:
|
|
```python
|
|
class note: # lowercase
|
|
class session_manager: # snake_case
|
|
class Micropub_Endpoint: # Mixed
|
|
```
|
|
|
|
---
|
|
|
|
### Functions and Methods
|
|
|
|
**Pattern**: `lowercase_with_underscores`
|
|
|
|
**Good**:
|
|
```python
|
|
def create_note(content):
|
|
pass
|
|
|
|
def get_note_by_slug(slug):
|
|
pass
|
|
|
|
def generate_rss_feed():
|
|
pass
|
|
```
|
|
|
|
**Bad**:
|
|
```python
|
|
def createNote(): # camelCase
|
|
def GetNoteBySlug(): # PascalCase
|
|
def generate_RSS_feed(): # Mixed case
|
|
```
|
|
|
|
---
|
|
|
|
### Variables
|
|
|
|
**Pattern**: `lowercase_with_underscores`
|
|
|
|
**Good**:
|
|
```python
|
|
note_count = 10
|
|
file_path = Path("data/notes")
|
|
session_token = generate_token()
|
|
```
|
|
|
|
**Bad**:
|
|
```python
|
|
noteCount = 10 # camelCase
|
|
FilePath = Path() # PascalCase
|
|
session_Token = "" # Mixed
|
|
```
|
|
|
|
---
|
|
|
|
### Constants
|
|
|
|
**Pattern**: `UPPERCASE_WITH_UNDERSCORES`
|
|
|
|
**Location**: Define at module level (top of file after imports)
|
|
|
|
**Good**:
|
|
```python
|
|
MAX_NOTE_LENGTH = 10000
|
|
DEFAULT_SESSION_LIFETIME = 30
|
|
INDIELOGIN_URL = "https://indielogin.com"
|
|
```
|
|
|
|
**Bad**:
|
|
```python
|
|
max_note_length = 10000 # lowercase
|
|
MaxNoteLength = 10000 # PascalCase
|
|
```
|
|
|
|
---
|
|
|
|
### Private Members
|
|
|
|
**Pattern**: Single leading underscore `_name` for internal use
|
|
|
|
**Good**:
|
|
```python
|
|
class Note:
|
|
def __init__(self, content):
|
|
self._content_hash = None # Internal attribute
|
|
|
|
def _calculate_hash(self): # Internal method
|
|
"""Calculate content hash (internal use)."""
|
|
pass
|
|
|
|
def save(self):
|
|
"""Public method."""
|
|
self._calculate_hash() # Can call private method
|
|
```
|
|
|
|
**Never use double underscore** `__name` (name mangling) unless you specifically need it (rare).
|
|
|
|
---
|
|
|
|
## Type Hints
|
|
|
|
### Standard
|
|
|
|
**Use type hints** for all function signatures in application code.
|
|
|
|
**Good**:
|
|
```python
|
|
from pathlib import Path
|
|
from typing import Optional, List
|
|
|
|
def create_note(content: str, published: bool = False) -> Note:
|
|
"""Create a new note."""
|
|
pass
|
|
|
|
def get_notes(limit: int = 10, offset: int = 0) -> List[Note]:
|
|
"""Get list of notes."""
|
|
pass
|
|
|
|
def find_note(slug: str) -> Optional[Note]:
|
|
"""Find note by slug, returns None if not found."""
|
|
pass
|
|
```
|
|
|
|
**Type Hint Guidelines**:
|
|
- Use built-in types when possible: `str`, `int`, `bool`, `list`, `dict`
|
|
- Use `typing` module for complex types: `Optional`, `List`, `Dict`, `Tuple`
|
|
- Python 3.11+ can use `list[Note]` instead of `List[Note]`
|
|
- Use `Optional[T]` for nullable types
|
|
- Use `->` for return type annotation
|
|
|
|
**Optional for Tests**: Type hints in tests are optional but encouraged.
|
|
|
|
---
|
|
|
|
## Documentation
|
|
|
|
### Docstrings
|
|
|
|
**Standard**: Use docstrings for all modules, classes, and functions.
|
|
|
|
**Format**: Google-style docstrings
|
|
|
|
**Module Docstring**:
|
|
```python
|
|
"""
|
|
Notes management module for StarPunk
|
|
|
|
This module handles creating, reading, updating, and deleting notes.
|
|
Notes are stored as markdown files with metadata in SQLite.
|
|
"""
|
|
```
|
|
|
|
**Function Docstring**:
|
|
```python
|
|
def create_note(content: str, slug: Optional[str] = None, published: bool = False) -> Note:
|
|
"""
|
|
Create a new note with the given content
|
|
|
|
Args:
|
|
content: Markdown content of the note
|
|
slug: Optional URL slug (generated if not provided)
|
|
published: Whether note is published (default: False)
|
|
|
|
Returns:
|
|
Created Note instance
|
|
|
|
Raises:
|
|
ValueError: If content is empty
|
|
FileExistsError: If slug already exists
|
|
|
|
Example:
|
|
>>> note = create_note("Hello world", slug="first-note")
|
|
>>> note.slug
|
|
'first-note'
|
|
"""
|
|
pass
|
|
```
|
|
|
|
**Class Docstring**:
|
|
```python
|
|
class Note:
|
|
"""
|
|
Represents a note with content and metadata
|
|
|
|
Attributes:
|
|
slug: URL-safe identifier
|
|
content: Markdown content
|
|
file_path: Path to markdown file
|
|
created_at: Creation timestamp
|
|
updated_at: Last modification timestamp
|
|
published: Publication status
|
|
"""
|
|
|
|
def __init__(self, slug: str, content: str):
|
|
"""
|
|
Initialize a new Note
|
|
|
|
Args:
|
|
slug: URL-safe identifier
|
|
content: Markdown content
|
|
"""
|
|
self.slug = slug
|
|
self.content = content
|
|
```
|
|
|
|
**Property Docstring**:
|
|
```python
|
|
@property
|
|
def html(self) -> str:
|
|
"""Rendered HTML content from markdown."""
|
|
return markdown.markdown(self.content)
|
|
```
|
|
|
|
**Short Functions**: One-line docstring is acceptable.
|
|
|
|
```python
|
|
def get_all_notes() -> List[Note]:
|
|
"""Get all notes from database."""
|
|
pass
|
|
```
|
|
|
|
---
|
|
|
|
### Comments
|
|
|
|
**Use sparingly** - code should be self-documenting via good naming.
|
|
|
|
**When to Comment**:
|
|
- Explain **why**, not **what**
|
|
- Document non-obvious business logic
|
|
- Clarify complex algorithms
|
|
- Note important gotchas or limitations
|
|
|
|
**Good Comment**:
|
|
```python
|
|
# IndieLogin requires state token to prevent CSRF attacks
|
|
state = generate_state_token()
|
|
|
|
# File operations must be atomic to prevent corruption
|
|
temp_path = file_path.with_suffix('.tmp')
|
|
temp_path.write_text(content)
|
|
temp_path.replace(file_path) # Atomic rename
|
|
```
|
|
|
|
**Bad Comment** (obvious from code):
|
|
```python
|
|
# Increment counter
|
|
counter += 1
|
|
|
|
# Create a note
|
|
note = Note(content)
|
|
```
|
|
|
|
**TODO Comments**: Acceptable for V1, should include context.
|
|
|
|
```python
|
|
# TODO: Add pagination when note count > 100
|
|
# TODO(username): Consider caching RSS feed (V2 feature)
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
### Exceptions
|
|
|
|
**Prefer specific exceptions** over generic `Exception`.
|
|
|
|
**Good**:
|
|
```python
|
|
def get_note(slug: str) -> Note:
|
|
"""Get note by slug."""
|
|
if not slug:
|
|
raise ValueError("Slug cannot be empty")
|
|
|
|
note = db.query(slug)
|
|
if not note:
|
|
raise FileNotFoundError(f"Note not found: {slug}")
|
|
|
|
return note
|
|
```
|
|
|
|
**Bad**:
|
|
```python
|
|
def get_note(slug: str) -> Note:
|
|
if not slug:
|
|
raise Exception("Bad slug") # Too generic
|
|
|
|
# Silently return None instead of raising exception
|
|
note = db.query(slug)
|
|
return note # Could be None
|
|
```
|
|
|
|
### Error Messages
|
|
|
|
**Be specific and actionable**:
|
|
|
|
**Good**:
|
|
```python
|
|
raise ValueError(
|
|
f"Invalid slug '{slug}': must contain only lowercase letters, "
|
|
f"numbers, and hyphens"
|
|
)
|
|
|
|
raise FileNotFoundError(
|
|
f"Note file not found: {file_path}. "
|
|
f"Database may be out of sync with filesystem."
|
|
)
|
|
```
|
|
|
|
**Bad**:
|
|
```python
|
|
raise ValueError("Invalid input")
|
|
raise FileNotFoundError("File missing")
|
|
```
|
|
|
|
### Exception Handling
|
|
|
|
**Catch specific exceptions**, not bare `except:`.
|
|
|
|
**Good**:
|
|
```python
|
|
try:
|
|
note = create_note(content)
|
|
except ValueError as e:
|
|
logger.error(f"Invalid note content: {e}")
|
|
return {"error": str(e)}, 400
|
|
except FileExistsError as e:
|
|
logger.error(f"Duplicate slug: {e}")
|
|
return {"error": "Note already exists"}, 409
|
|
```
|
|
|
|
**Bad**:
|
|
```python
|
|
try:
|
|
note = create_note(content)
|
|
except: # Too broad, catches everything including KeyboardInterrupt
|
|
return {"error": "Something went wrong"}, 500
|
|
```
|
|
|
|
---
|
|
|
|
## Flask-Specific Patterns
|
|
|
|
### Route Decorators
|
|
|
|
**Pattern**: One route per function, clear HTTP methods.
|
|
|
|
**Good**:
|
|
```python
|
|
@app.route('/api/notes', methods=['GET'])
|
|
def list_notes():
|
|
"""List all published notes."""
|
|
notes = get_published_notes()
|
|
return jsonify([note.to_dict() for note in notes])
|
|
|
|
@app.route('/api/notes', methods=['POST'])
|
|
def create_note_endpoint():
|
|
"""Create a new note."""
|
|
data = request.get_json()
|
|
note = create_note(data['content'])
|
|
return jsonify(note.to_dict()), 201
|
|
```
|
|
|
|
**Bad**:
|
|
```python
|
|
@app.route('/api/notes', methods=['GET', 'POST']) # Multiple methods in one function
|
|
def notes():
|
|
if request.method == 'GET':
|
|
...
|
|
elif request.method == 'POST':
|
|
...
|
|
```
|
|
|
|
### Request Handling
|
|
|
|
**Validate input explicitly**:
|
|
|
|
**Good**:
|
|
```python
|
|
@app.route('/api/notes', methods=['POST'])
|
|
def create_note_endpoint():
|
|
"""Create a new note."""
|
|
data = request.get_json()
|
|
|
|
# Validate required fields
|
|
if not data or 'content' not in data:
|
|
return {"error": "Missing required field: content"}, 400
|
|
|
|
content = data['content'].strip()
|
|
if not content:
|
|
return {"error": "Content cannot be empty"}, 400
|
|
|
|
# Create note
|
|
try:
|
|
note = create_note(content)
|
|
return jsonify(note.to_dict()), 201
|
|
except ValueError as e:
|
|
return {"error": str(e)}, 400
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Standards
|
|
|
|
### Test Naming
|
|
|
|
**Pattern**: `test_{function_name}_{scenario}`
|
|
|
|
**Good**:
|
|
```python
|
|
def test_create_note_success():
|
|
"""Test creating a note with valid content."""
|
|
pass
|
|
|
|
def test_create_note_empty_content():
|
|
"""Test creating a note with empty content raises ValueError."""
|
|
pass
|
|
|
|
def test_get_note_by_slug_not_found():
|
|
"""Test getting non-existent note raises FileNotFoundError."""
|
|
pass
|
|
```
|
|
|
|
### Test Structure
|
|
|
|
**Use Arrange-Act-Assert pattern**:
|
|
|
|
```python
|
|
def test_create_note_success():
|
|
"""Test creating a note with valid content."""
|
|
# Arrange
|
|
content = "This is a test note"
|
|
slug = "test-note"
|
|
|
|
# Act
|
|
note = create_note(content, slug=slug)
|
|
|
|
# Assert
|
|
assert note.slug == slug
|
|
assert note.content == content
|
|
assert note.published is False
|
|
```
|
|
|
|
### Fixtures
|
|
|
|
**Use pytest fixtures** for common setup:
|
|
|
|
```python
|
|
@pytest.fixture
|
|
def sample_note():
|
|
"""Create a sample note for testing."""
|
|
return create_note("Sample content", slug="sample")
|
|
|
|
def test_note_to_dict(sample_note):
|
|
"""Test converting note to dictionary."""
|
|
data = sample_note.to_dict()
|
|
assert data['slug'] == 'sample'
|
|
assert 'content' in data
|
|
```
|
|
|
|
---
|
|
|
|
## File Organization
|
|
|
|
### Module Structure
|
|
|
|
**Standard order within a Python file**:
|
|
|
|
1. Module docstring
|
|
2. Imports (standard → third-party → local)
|
|
3. Constants
|
|
4. Exception classes (if any)
|
|
5. Classes
|
|
6. Functions
|
|
7. Main execution block (if applicable)
|
|
|
|
**Example**:
|
|
```python
|
|
"""
|
|
Notes management module
|
|
|
|
This module handles note CRUD operations.
|
|
"""
|
|
|
|
# Standard library
|
|
import os
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
# Third-party
|
|
import markdown
|
|
from flask import current_app
|
|
|
|
# Local
|
|
from starpunk.database import get_db
|
|
from starpunk.utils import generate_slug
|
|
|
|
# Constants
|
|
MAX_NOTE_LENGTH = 10000
|
|
DEFAULT_PUBLISHED_STATUS = False
|
|
|
|
|
|
# Exception classes
|
|
class NoteError(Exception):
|
|
"""Base exception for note operations."""
|
|
pass
|
|
|
|
|
|
class DuplicateSlugError(NoteError):
|
|
"""Raised when note slug already exists."""
|
|
pass
|
|
|
|
|
|
# Classes
|
|
class Note:
|
|
"""Represents a note."""
|
|
pass
|
|
|
|
|
|
# Functions
|
|
def create_note(content: str) -> Note:
|
|
"""Create a new note."""
|
|
pass
|
|
|
|
|
|
def get_note(slug: str) -> Note:
|
|
"""Get note by slug."""
|
|
pass
|
|
```
|
|
|
|
---
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
### Don't Repeat Yourself (DRY)
|
|
|
|
**Bad**:
|
|
```python
|
|
def get_note_as_html(slug):
|
|
note = db.query(slug)
|
|
html = markdown.markdown(note.content)
|
|
return html
|
|
|
|
def get_note_for_rss(slug):
|
|
note = db.query(slug)
|
|
html = markdown.markdown(note.content)
|
|
return html
|
|
```
|
|
|
|
**Good**:
|
|
```python
|
|
def get_note(slug: str) -> Note:
|
|
"""Get note by slug."""
|
|
return db.query(slug)
|
|
|
|
def render_markdown(content: str) -> str:
|
|
"""Render markdown to HTML."""
|
|
return markdown.markdown(content)
|
|
|
|
# Use in different contexts
|
|
def get_note_as_html(slug: str) -> str:
|
|
note = get_note(slug)
|
|
return render_markdown(note.content)
|
|
```
|
|
|
|
### Avoid Magic Numbers
|
|
|
|
**Bad**:
|
|
```python
|
|
if len(content) > 10000:
|
|
raise ValueError("Content too long")
|
|
|
|
time.sleep(86400) # What is 86400?
|
|
```
|
|
|
|
**Good**:
|
|
```python
|
|
MAX_NOTE_LENGTH = 10000
|
|
SECONDS_PER_DAY = 86400
|
|
|
|
if len(content) > MAX_NOTE_LENGTH:
|
|
raise ValueError(f"Content exceeds {MAX_NOTE_LENGTH} characters")
|
|
|
|
time.sleep(SECONDS_PER_DAY)
|
|
```
|
|
|
|
### Avoid Mutable Default Arguments
|
|
|
|
**Bad**:
|
|
```python
|
|
def create_note(content: str, tags: list = []): # Mutable default
|
|
tags.append('note') # Modifies shared default list
|
|
return Note(content, tags)
|
|
```
|
|
|
|
**Good**:
|
|
```python
|
|
def create_note(content: str, tags: Optional[List[str]] = None) -> Note:
|
|
if tags is None:
|
|
tags = []
|
|
tags.append('note')
|
|
return Note(content, tags)
|
|
```
|
|
|
|
### Avoid God Objects
|
|
|
|
**Bad**:
|
|
```python
|
|
class StarPunk:
|
|
"""Does everything."""
|
|
def create_note(self): pass
|
|
def render_html(self): pass
|
|
def generate_rss(self): pass
|
|
def authenticate(self): pass
|
|
def send_email(self): pass
|
|
# 50 more methods...
|
|
```
|
|
|
|
**Good**:
|
|
```python
|
|
# Separate concerns into focused modules
|
|
class Note:
|
|
"""Represents a note."""
|
|
pass
|
|
|
|
class NoteManager:
|
|
"""Manages note CRUD operations."""
|
|
pass
|
|
|
|
class FeedGenerator:
|
|
"""Generates RSS feeds."""
|
|
pass
|
|
```
|
|
|
|
---
|
|
|
|
## Code Quality Tools
|
|
|
|
### Black (Formatter)
|
|
|
|
**Required**: All code MUST be formatted with Black before commit.
|
|
|
|
```bash
|
|
# Format code
|
|
.venv/bin/black starpunk/ tests/
|
|
|
|
# Check formatting
|
|
.venv/bin/black --check starpunk/ tests/
|
|
```
|
|
|
|
### Flake8 (Linter)
|
|
|
|
**Required**: All code MUST pass flake8 checks.
|
|
|
|
```bash
|
|
# Lint code
|
|
.venv/bin/flake8 starpunk/ tests/
|
|
|
|
# Configuration in setup.cfg or pyproject.toml
|
|
```
|
|
|
|
**Configuration** (`setup.cfg`):
|
|
```ini
|
|
[flake8]
|
|
max-line-length = 88
|
|
extend-ignore = E203, W503
|
|
exclude =
|
|
.venv,
|
|
__pycache__,
|
|
data,
|
|
.git
|
|
```
|
|
|
|
### Mypy (Type Checker)
|
|
|
|
**Recommended**: Run mypy for type checking (optional for V1).
|
|
|
|
```bash
|
|
# Type check
|
|
.venv/bin/mypy starpunk/
|
|
```
|
|
|
|
**Configuration** (`pyproject.toml`):
|
|
```toml
|
|
[tool.mypy]
|
|
python_version = "3.11"
|
|
warn_return_any = true
|
|
warn_unused_configs = true
|
|
disallow_untyped_defs = false # Set to true for strict mode
|
|
```
|
|
|
|
### Pytest (Testing)
|
|
|
|
**Required**: All code MUST have tests with >80% coverage.
|
|
|
|
```bash
|
|
# Run tests
|
|
.venv/bin/pytest
|
|
|
|
# With coverage
|
|
.venv/bin/pytest --cov=starpunk --cov-report=html tests/
|
|
```
|
|
|
|
---
|
|
|
|
## Pre-Commit Checklist
|
|
|
|
Before committing code, verify:
|
|
|
|
```bash
|
|
# 1. Format code
|
|
.venv/bin/black starpunk/ tests/
|
|
|
|
# 2. Lint code
|
|
.venv/bin/flake8 starpunk/ tests/
|
|
|
|
# 3. Run tests
|
|
.venv/bin/pytest
|
|
|
|
# 4. Check coverage
|
|
.venv/bin/pytest --cov=starpunk tests/
|
|
|
|
# 5. Type check (optional)
|
|
.venv/bin/mypy starpunk/
|
|
```
|
|
|
|
**All checks must pass** before code is committed.
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
**Key Principles**:
|
|
1. Use Black for formatting (88 char lines)
|
|
2. Follow PEP 8 naming conventions
|
|
3. Add type hints to all function signatures
|
|
4. Write docstrings for all public APIs
|
|
5. Handle errors with specific exceptions
|
|
6. Keep functions focused and small
|
|
7. Test everything (>80% coverage)
|
|
8. Make code self-documenting
|
|
|
|
**Tools**:
|
|
- Black (required) - formatting
|
|
- Flake8 (required) - linting
|
|
- Pytest (required) - testing
|
|
- Mypy (recommended) - type checking
|
|
|
|
**Remember**: "Every line of code must justify its existence."
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [PEP 8 - Style Guide](https://peps.python.org/pep-0008/)
|
|
- [PEP 257 - Docstring Conventions](https://peps.python.org/pep-0257/)
|
|
- [PEP 484 - Type Hints](https://peps.python.org/pep-0484/)
|
|
- [Black Documentation](https://black.readthedocs.io/)
|
|
- [Flake8 Documentation](https://flake8.pycqa.org/)
|
|
- [Mypy Documentation](https://mypy.readthedocs.io/)
|
|
- [Pytest Documentation](https://docs.pytest.org/)
|
|
- [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html)
|