Skip to content

Coding Standards

Code Style

  • Python 3.12+ syntax and features
  • PEP 8, enforced by ruff (100-character line length, see pyproject.toml)
  • Type hints on all functions and methods
  • Google-style docstrings for all public classes and functions
  • Meaningful test coverage — the project targets 80%+ (see CLAUDE.md for current per-module numbers)

Formatting and Linting

ruff handles both formatting and linting; pre-commit also runs mypy.

# Format code
task lint

# Check formatting without modifying files
task lint:check

# Run all pre-commit hooks (ruff format, mypy, trailing whitespace, etc.)
task pre-commit

# Run ruff directly if you need more control
uv run ruff check src tests
uv run ruff format src tests

Docstrings

  • Google-style, as configured in [tool.ruff.lint.pydocstyle]
  • Document Args, Returns, and Raises for public functions
  • Include a short usage example in the docstring where it clarifies behavior non-obviously — don't restate what the signature already says

Writing Tests

  • Unit tests live in tests/unit/, integration tests in tests/integration/ (see tests/conftest.py for shared fixtures)
  • Use descriptive test names that describe the scenario, not just the function under test
  • Cover both the success path and the relevant failure/edge cases
  • Mock external dependencies (SSH via pexpect, filesystem operations) — see tests/unit/test_gear.py for the established mocking patterns

Example:

def test_config_validation_success():
    """Test successful configuration validation."""
    config_data = {
        "fetch": {
            "test_task": {
                "label": "test",
                "summary": "Test task",
                "host": "user@host",
                "source": "/remote/path/",
                "target": "./local/path/",
                "options": ["-av"],
                "enabled": True,
            }
        }
    }

    config = FnbConfig.model_validate(config_data)
    assert len(config.fetch) == 1
    assert config.fetch["test_task"].label == "test"

A pitfall worth knowing

fetcher.py, backuper.py, and reader.py import gear/env functions with from fnb.gear import run_rsync, verify_directory — a from-import binds the name at import time. Patching fnb.gear.run_rsync in a test will not affect code that already imported it this way; patch the consumer's namespace instead (e.g. fnb.fetcher.run_rsync). See tests/integration/test_integration.py's integrated_mocks fixture for the pattern.