Skip to content

Config Loader

YAML and JSON configuration file parsing.

config_loader

Load diagram configurations from YAML/JSON files.

ConfigLoader

ConfigLoader(
    *,
    emit_validation_output: bool = True,
    board_selection_strategy: BoardSelectionStrategy
    | None = None,
)

Load and parse diagram configurations from files.

Supports loading diagrams from YAML and JSON configuration files. Handles predefined device types from the device registry and custom device definitions with automatic wire color assignment.

Examples:

>>> loader = ConfigLoader()
>>> diagram = loader.load_from_file("config.yaml")
>>> print(diagram.title)
My GPIO Diagram

Initialize config loader behavior.

PARAMETER DESCRIPTION
emit_validation_output

Whether to print graph validation messages to stdout while loading configurations.

TYPE: bool DEFAULT: True

board_selection_strategy

Strategy used to resolve board aliases.

TYPE: BoardSelectionStrategy | None DEFAULT: None

Source code in src/pinviz/config_loader.py
def __init__(
    self,
    *,
    emit_validation_output: bool = True,
    board_selection_strategy: BoardSelectionStrategy | None = None,
) -> None:
    """Initialize config loader behavior.

    Args:
        emit_validation_output: Whether to print graph validation messages
            to stdout while loading configurations.
        board_selection_strategy: Strategy used to resolve board aliases.
    """
    self.emit_validation_output = emit_validation_output
    self._board_selection_strategy = board_selection_strategy or AliasBoardSelectionStrategy()

load_from_dict

load_from_dict(config: dict[str, Any]) -> Diagram

Load a diagram from a configuration dictionary.

Expected structure: { "title": "My Diagram", "board": "raspberry_pi_5" or {"name": "...", ...}, "devices": [ {"type": "bh1750", "name": "Light Sensor"}, {"name": "Custom Device", "pins": [...], ...} ], "connections": [ {"board_pin": 1, "device": "Light Sensor", "device_pin": "VCC"}, ... ] }

PARAMETER DESCRIPTION
config

Configuration dictionary

TYPE: dict[str, Any]

RETURNS DESCRIPTION
Diagram

Diagram object

RAISES DESCRIPTION
ValueError

If configuration fails schema validation

Source code in src/pinviz/config_loader.py
def load_from_dict(self, config: dict[str, Any]) -> Diagram:
    """
    Load a diagram from a configuration dictionary.

    Expected structure:
    {
        "title": "My Diagram",
        "board": "raspberry_pi_5" or {"name": "...", ...},
        "devices": [
            {"type": "bh1750", "name": "Light Sensor"},
            {"name": "Custom Device", "pins": [...], ...}
        ],
        "connections": [
            {"board_pin": 1, "device": "Light Sensor", "device_pin": "VCC"},
            ...
        ]
    }

    Args:
        config: Configuration dictionary

    Returns:
        Diagram object

    Raises:
        ValueError: If configuration fails schema validation
    """
    # Check for None or non-dict config
    if config is None or not isinstance(config, dict):
        log.error("invalid_config_type", config_type=type(config).__name__)
        raise ValueError(
            "Configuration validation failed:\n"
            "  • Config must be a dictionary with required fields "
            "(title, board, devices, connections)"
        )

    # Validate configuration against schema
    try:
        validated_config = validate_config(config)
        log.debug(
            "config_schema_validated",
            title=validated_config.title,
            device_count=len(validated_config.devices),
            connection_count=len(validated_config.connections),
        )
    except ValidationError as e:
        # Format validation errors for better readability
        error_messages = []
        for error in e.errors():
            field_path = " -> ".join(str(loc) for loc in error["loc"])
            error_messages.append(f"  • {field_path}: {error['msg']}")

        log.error(
            "config_schema_validation_failed",
            error_count=len(error_messages),
            errors=error_messages,
        )
        raise ValueError(
            "Configuration validation failed:\n" + "\n".join(error_messages)
        ) from e

    # Load board
    board_config = validated_config.board
    if isinstance(board_config, str):
        board = self._load_board_by_name(board_config)
        log.debug("board_loaded", board_name=board.name)
    else:
        log.error("custom_board_not_supported")
        raise ValueError("Custom board definitions not yet supported")

    # Load devices
    device_configs = validated_config.devices
    diagram_devices = []

    log.debug("loading_devices", device_count=len(device_configs))
    for dev_config in device_configs:
        device = self._load_device(dev_config)
        diagram_devices.append(device)
        log.debug(
            "device_loaded",
            device_name=device.name,
            pin_count=len(device.pins),
            device_type=dev_config.get("type", "custom"),
        )

    # Load connections with smart pin assignment
    connection_configs = [
        connection.model_dump(by_alias=True, exclude_none=True)
        for connection in validated_config.connections
    ]
    connections = []

    # Create pin assigner for automatic role-based pin distribution
    pin_assigner = PinAssigner(board)

    log.debug("loading_connections", connection_count=len(connection_configs))
    for conn_config in connection_configs:
        connection = self._load_connection(conn_config, pin_assigner)
        connections.append(connection)

    # Validate graph structure
    log.debug("validating_graph_structure")
    graph = ConnectionGraph(diagram_devices, connections)
    validation_issues = self._validate_graph(graph, diagram_devices, connections)

    # Check for critical errors
    errors = [issue for issue in validation_issues if issue.level == ValidationLevel.ERROR]
    warnings = [issue for issue in validation_issues if issue.level == ValidationLevel.WARNING]

    if errors:
        if self.emit_validation_output:
            self._report_validation_errors(errors)
        log.error("config_validation_failed", error_count=len(errors))
        raise ValueError("Configuration has critical errors")

    if warnings and self.emit_validation_output:
        self._report_validation_warnings(warnings)

    # Parse theme
    theme_str = validated_config.theme
    try:
        theme = Theme(theme_str.lower())
    except ValueError:
        log.warning("invalid_theme", theme=theme_str, using_default="light")
        theme = Theme.LIGHT

    # Create diagram via the shared builder so all entry points assemble the
    # same core model and presentation options.
    options = DiagramOptions(
        show_legend=validated_config.show_legend,
        show_gpio_diagram=validated_config.show_gpio_diagram,
        show_title=validated_config.show_title,
        show_board_name=validated_config.show_board_name,
        theme=theme,
    )
    diagram = (
        DiagramBuilder(self._board_selection_strategy)
        .with_title(validated_config.title)
        .with_board(board)
        .with_devices(diagram_devices)
        .with_connections(connections)
        .with_options(options)
        .build()
    )

    # Run electrical safety validation (voltage, pin compatibility, etc.)
    electrical_issues = DiagramValidator().validate(diagram)
    electrical_warnings = [
        issue
        for issue in electrical_issues
        if issue.level in (ValidationLevel.WARNING, ValidationLevel.ERROR)
    ]

    if electrical_warnings and self.emit_validation_output:
        self._report_validation_warnings(electrical_warnings)

    log.info(
        "diagram_config_loaded",
        title=diagram.title,
        board=board.name,
        device_count=len(diagram_devices),
        connection_count=len(connections),
    )

    return diagram

load_from_file

load_from_file(config_path: str | Path) -> Diagram

Load a diagram from a YAML or JSON configuration file.

PARAMETER DESCRIPTION
config_path

Path to configuration file (.yaml, .yml, or .json)

TYPE: str | Path

RETURNS DESCRIPTION
Diagram

Diagram object

RAISES DESCRIPTION
ValueError

If file format is not supported or config is invalid

Source code in src/pinviz/config_loader.py
def load_from_file(self, config_path: str | Path) -> Diagram:
    """
    Load a diagram from a YAML or JSON configuration file.

    Args:
        config_path: Path to configuration file (.yaml, .yml, or .json)

    Returns:
        Diagram object

    Raises:
        ValueError: If file format is not supported or config is invalid
    """
    path = Path(config_path)

    log.debug("loading_config_file", config_path=str(path), format=path.suffix)

    if not path.exists():
        log.error("config_file_not_found", config_path=str(path))
        raise FileNotFoundError(f"Configuration file not found: {config_path}")

    # Validate file size to prevent memory issues
    file_size = path.stat().st_size
    if file_size > MAX_CONFIG_FILE_SIZE:
        log.error(
            "config_file_too_large",
            config_path=str(path),
            size_bytes=file_size,
            max_bytes=MAX_CONFIG_FILE_SIZE,
        )
        max_mb = MAX_CONFIG_FILE_SIZE // (1024 * 1024)
        size_mb = file_size / (1024 * 1024)
        raise ValueError(
            format_config_error(
                "file_too_large",
                detail=f"{size_mb:.1f}MB (max: {max_mb}MB)",
            )
        )

    # Load file based on extension
    if path.suffix in [".yaml", ".yml"]:
        with open(path) as f:
            config = yaml.safe_load(f)
        log.debug("yaml_config_parsed", config_path=str(path))
    elif path.suffix == ".json":
        with open(path) as f:
            config = json.load(f)
        log.debug("json_config_parsed", config_path=str(path))
    else:
        log.error("unsupported_file_format", format=path.suffix, config_path=str(path))
        raise ValueError(
            format_config_error(
                "invalid_yaml" if path.suffix in [".yaml", ".yml"] else "invalid_json",
                detail=f"Unsupported file extension: {path.suffix}",
            )
        )

    return self.load_from_dict(config)

load_diagram

load_diagram(
    config_path: str | Path,
    *,
    emit_validation_output: bool = True,
) -> Diagram

Convenience function to load a diagram from a file.

PARAMETER DESCRIPTION
config_path

Path to YAML or JSON configuration file

TYPE: str | Path

emit_validation_output

Whether to print graph validation details

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
Diagram

Diagram object

Source code in src/pinviz/config_loader.py
def load_diagram(config_path: str | Path, *, emit_validation_output: bool = True) -> Diagram:
    """
    Convenience function to load a diagram from a file.

    Args:
        config_path: Path to YAML or JSON configuration file
        emit_validation_output: Whether to print graph validation details

    Returns:
        Diagram object
    """
    loader = ConfigLoader(emit_validation_output=emit_validation_output)
    return loader.load_from_file(config_path)