Skip to content

Code Style Guide

Coding conventions and standards for pib3.

Python Style

We follow PEP 8 with these specifics:

Line Length

  • Maximum 100 characters (relaxed from PEP 8's 79)
  • Docstrings and comments: 80 characters preferred

Imports

# Standard library
import math
import os
from pathlib import Path
from typing import Dict, List, Optional

# Third-party
import numpy as np
from PIL import Image

# Local
from pib3.config import TrajectoryConfig
from pib3.types import Sketch, Stroke

Naming Conventions

# Modules: lowercase with underscores
trajectory.py
proto_converter.py

# Classes: CamelCase
class RobotBackend:
class TrajectoryConfig:

# Functions/methods: lowercase with underscores
def image_to_sketch():
def get_joint():

# Constants: UPPERCASE with underscores
WEBOTS_OFFSET = 0.0
DEFAULT_TOLERANCE = 0.001

# Private: leading underscore
def _internal_helper():
_cached_result = None

Type Hints

Always include type hints for public functions:

from typing import Dict, List, Optional, Union
import numpy as np

def set_joint(
    self,
    name: str,
    position: float,
    unit: str = "percent",
    async_: bool = True,
) -> bool:
    """Set a joint position."""
    ...

def get_joints(
    self,
    names: Optional[List[str]] = None,
) -> Dict[str, float]:
    """Get multiple joint positions."""
    ...

Common Types

from typing import Callable, Dict, List, Optional, Tuple, Union
from pathlib import Path
import numpy as np
from numpy.typing import NDArray

# Path types
ImageInput = Union[str, Path, np.ndarray, "PIL.Image.Image"]

# Callback types
ProgressCallback = Callable[[int, int], None]
IKProgressCallback = Callable[[int, int, bool], None]

# Array types
JointArray = NDArray[np.float64]  # Shape: (n_joints,)
WaypointArray = NDArray[np.float64]  # Shape: (n_waypoints, n_joints)

Docstrings

Use Google-style docstrings:

def image_to_sketch(
    image: ImageInput,
    config: Optional[ImageConfig] = None,
) -> Sketch:
    """
    Convert an image to a Sketch for trajectory generation.

    Processes the input image to extract contours, simplifies them,
    and returns a normalized Sketch object.

    Args:
        image: Input image. Can be:
            - Path to image file (str or Path)
            - NumPy array (grayscale, RGB, or RGBA)
            - PIL Image object
        config: Optional processing configuration. If None,
            uses default ImageConfig.

    Returns:
        A Sketch object containing normalized strokes.

    Raises:
        FileNotFoundError: If image path doesn't exist.
        ValueError: If image format is not supported.

    Example:
        >>> sketch = image_to_sketch("drawing.png")
        >>> print(f"Extracted {len(sketch)} strokes")
        Extracted 15 strokes

        >>> from pib3 import ImageConfig
        >>> config = ImageConfig(threshold=100)
        >>> sketch = image_to_sketch("light_sketch.jpg", config)
    """

Class Docstrings

class Trajectory:
    """
    Container for robot joint trajectories.

    Stores a sequence of joint positions (waypoints) along with
    metadata about the trajectory source and generation.

    Attributes:
        joint_names: List of joint names in order.
        waypoints: Array of shape (n_waypoints, n_joints) in radians.
        metadata: Dictionary with source info, timestamps, etc.

    Example:
        >>> trajectory = Trajectory.from_json("path.json")
        >>> print(f"Trajectory has {len(trajectory)} waypoints")
        >>> trajectory.to_json("output.json")
    """

Error Handling

Use Specific Exceptions

# Good: specific exception with context
if not path.exists():
    raise FileNotFoundError(f"Image file not found: {path}")

if threshold < 0 or threshold > 255:
    raise ValueError(f"Threshold must be 0-255, got {threshold}")

# Bad: generic exception
raise Exception("Something went wrong")

Document Exceptions

def connect(self) -> None:
    """
    Connect to the robot.

    Raises:
        ConnectionError: If unable to connect within timeout.
        ValueError: If host address is invalid.
    """

Classes

Dataclasses for Configuration

from dataclasses import dataclass, field
from typing import Optional

@dataclass
class PaperConfig:
    """Configuration for drawing paper position."""

    center_x: float = 0.15
    center_y: float = 0.15
    height_z: float = 0.74
    size: float = 0.12
    drawing_scale: float = 0.9

    def __post_init__(self):
        if self.size <= 0:
            raise ValueError(f"Paper size must be positive, got {self.size}")

Abstract Base Classes

from abc import ABC, abstractmethod

class RobotBackend(ABC):
    """Abstract base class for robot control backends."""

    @abstractmethod
    def connect(self) -> None:
        """Connect to the robot."""
        ...

    @abstractmethod
    def disconnect(self) -> None:
        """Disconnect from the robot."""
        ...

    # Concrete method with default implementation
    def __enter__(self):
        self.connect()
        return self

    def __exit__(self, *args):
        self.disconnect()

Testing

Test Structure

import pytest
from pib3 import image_to_sketch, Sketch

class TestImageToSketch:
    """Tests for image_to_sketch function."""

    def test_from_file_path(self, tmp_path):
        """Should load image from file path."""
        # Arrange
        image_path = tmp_path / "test.png"
        create_test_image(image_path)

        # Act
        sketch = image_to_sketch(image_path)

        # Assert
        assert isinstance(sketch, Sketch)
        assert len(sketch) > 0

    def test_invalid_path_raises(self):
        """Should raise FileNotFoundError for missing file."""
        with pytest.raises(FileNotFoundError):
            image_to_sketch("nonexistent.png")

    @pytest.mark.parametrize("threshold", [0, 128, 255])
    def test_threshold_values(self, threshold, sample_image):
        """Should accept valid threshold values."""
        config = ImageConfig(threshold=threshold)
        sketch = image_to_sketch(sample_image, config)
        assert isinstance(sketch, Sketch)

Fixtures

import pytest
import numpy as np

@pytest.fixture
def sample_image():
    """Create a simple test image."""
    img = np.zeros((100, 100), dtype=np.uint8)
    img[25:75, 25:75] = 255  # White square
    return img

@pytest.fixture
def sample_sketch():
    """Create a simple test sketch."""
    from pib3 import Sketch, Stroke, Point
    return Sketch([
        Stroke([Point(0.0, 0.0), Point(1.0, 1.0)]),
    ])

Comments

When to Comment

# Good: clarify complex logic
# Use damped least squares to avoid singularities near joint limits
J_damped = J.T @ np.linalg.inv(J @ J.T + damping * np.eye(6))

# Bad: restating the code
# Subtract 1.0 from radians
webots_pos = radians - 1.0

TODO Comments

# TODO(username): Implement caching for repeated IK solutions
# TODO: Add support for right arm drawing (issue #42)

File Organization

"""
Module docstring explaining purpose.

This module provides functionality for X.
"""

# Imports (grouped and sorted)
import ...

# Constants
CONSTANT = value

# Type aliases
MyType = ...

# Classes
class MyClass:
    ...

# Functions
def my_function():
    ...

# Private helpers
def _helper():
    ...

# Module-level code (if any)
if __name__ == "__main__":
    ...