Skip to content

chemigram.core.dtstyle

chemigram.core.dtstyle

Parse darktable .dtstyle files.

A .dtstyle is darktable's style export format: XML containing one or more single-module configurations. Calibrated to darktable 5.4.1 (see tests/fixtures/README.md and docs/adr/TA.md contracts/dtstyle-schema).

Binary parameters (op_params, blendop_params) are opaque blobs (ADR-008). They are never decoded — they pass through verbatim as strings.

XML parsing uses defusedxml for safety against malicious or malformed input. (Adopted as a defensive default; not pinned to an ADR.)

Public API
  • :func:parse_dtstyle — parse a file from disk
  • :class:DtstyleEntry, :class:PluginEntry — frozen dataclasses
  • :class:DtstyleParseError — raised on malformed input

DtstyleParseError

Bases: Exception

Raised when a .dtstyle file cannot be parsed.

PluginEntry dataclass

PluginEntry(operation, num, module, op_params, blendop_params, blendop_version, multi_priority, multi_name, enabled)

One <plugin> element from a .dtstyle file.

Calibrated to darktable 5.4.1. Binary blobs (op_params, blendop_params) are kept as strings and NEVER decoded (ADR-008).

DtstyleEntry dataclass

DtstyleEntry(name, description, iop_list, plugins)

A parsed .dtstyle file.

parse_dtstyle

parse_dtstyle(path)

Parse a .dtstyle file from disk.

Parameters:

Name Type Description Default
path Path

Path to a .dtstyle file.

required

Returns:

Name Type Description
A DtstyleEntry

class:DtstyleEntry capturing the file's contents. Plugins

DtstyleEntry

are returned as a tuple in document order.

Raises:

Type Description
DtstyleParseError

malformed XML; missing required elements (e.g., no <plugin> inside <style>); missing required child elements within a <plugin>; <enabled> not 0/1.

FileNotFoundError

path does not exist.

Source code in src/chemigram/core/dtstyle.py
def parse_dtstyle(path: Path) -> DtstyleEntry:
    """Parse a .dtstyle file from disk.

    Args:
        path: Path to a .dtstyle file.

    Returns:
        A :class:`DtstyleEntry` capturing the file's contents. Plugins
        are returned as a tuple in document order.

    Raises:
        DtstyleParseError: malformed XML; missing required elements
            (e.g., no ``<plugin>`` inside ``<style>``); missing required
            child elements within a ``<plugin>``; ``<enabled>`` not 0/1.
        FileNotFoundError: ``path`` does not exist.
    """
    if not path.exists():
        raise FileNotFoundError(path)

    try:
        tree = ElementTree.parse(path)
    except ElementTree.ParseError as exc:
        raise DtstyleParseError(f"{path}: malformed XML: {exc}") from exc

    root = tree.getroot()
    if root.tag != "darktable_style":
        raise DtstyleParseError(f"{path}: root element must be <darktable_style>, got <{root.tag}>")

    info = root.find("info")
    if info is None:
        raise DtstyleParseError(f"{path}: missing <info>")

    name = _require_text(info, "name", path)
    description = _optional_text(info, "description") or ""
    iop_list = _optional_text(info, "iop_list")

    style = root.find("style")
    if style is None:
        raise DtstyleParseError(f"{path}: missing <style>")

    plugin_elems = style.findall("plugin")
    if not plugin_elems:
        raise DtstyleParseError(f"{path}: <style> must contain at least one <plugin>")

    # Parse all plugins, then filter out darktable's auto-applied entries
    # (multi_name prefixed "_builtin_") per ADR-010. Phase 0 working
    # notebook recommends this safety-net filter to protect against
    # contributor authoring errors (the "first attempt" failure mode where
    # a contaminated dtstyle exports the full default pipeline).
    plugins_all = tuple(_parse_plugin(p, path) for p in plugin_elems)
    plugins = tuple(p for p in plugins_all if not p.multi_name.startswith("_builtin_"))

    if not plugins:
        filtered = len(plugins_all)
        raise DtstyleParseError(
            f"{path}: no user-authored <plugin> entries "
            f"(filtered {filtered} _builtin_* entries; per ADR-010, user "
            "entries are identified by empty <multi_name>)"
        )

    return DtstyleEntry(
        name=name,
        description=description,
        iop_list=iop_list,
        plugins=plugins,
    )