Skip to content

Backends

The base contract every backend implements, followed by each built-in format backend.

Base contract

yaconfiglib.backends.base.ConfigBackend

Bases: Protocol

Base contract for a pluggable configuration format backend.

A backend is responsible for turning a source (typically a file path, but also strings, streams, or in-memory data depending on the backend) into a Python object — usually a dict. Backends are looked up and instantiated automatically by :class:~yaconfiglib.loader.ConfigLoader based on either an explicit loader= name/instance or by matching :attr:PATHNAME_REGEX against the source path.

To implement a new backend, subclass :class:ConfigBackend and override :meth:load (required) and optionally :meth:dumps (for round-trip serialization support). Subclasses are auto-discovered — simply importing the module that defines the subclass registers it; no explicit registry call is needed. See yaconfiglib.backends for the built-in implementations (YAML, TOML, JSON, INI, dotenv, env, command, python, jinja2).

Class attributes

PATHNAME_REGEX: Compiled regex matched against a path's filename (or, for scheme-based backends like CommandBackend, the full path string) to decide whether this backend can handle a given source. Set to None for backends that are only selected explicitly by name (e.g. EnvVarBackend). NAME: Explicit registry name used by loader="name" lookups. If unset, the name is derived from the class name by lowercasing it and stripping a trailing Loader/Config suffix. DEFAULT_ENCODING: Text encoding used when a backend reads a file and no explicit encoding= is supplied. DEFAULT_PATH_FACTORY: Path constructor used to build path objects when the caller passes a bare string rather than a Path.

DEFAULT_ENCODING = 'utf-8' class-attribute instance-attribute

DEFAULT_PATH_FACTORY = _LocalPath class-attribute instance-attribute

NAME = None class-attribute instance-attribute

PATHNAME_REGEX = None class-attribute instance-attribute

__call__(*args, **kwds)

Dispatch to :meth:_yaml_tag_constructor when used as a PyYAML tag constructor.

This lets a backend instance be registered directly with yaml.Loader.add_constructor (e.g. for !include/!load tags) — PyYAML calls constructors as constructor(loader, node), which this method recognizes and routes accordingly.

Source code in src/yaconfiglib/backends/base.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def __call__(self, *args, **kwds):
    """Dispatch to :meth:`_yaml_tag_constructor` when used as a PyYAML tag constructor.

    This lets a backend instance be registered directly with
    ``yaml.Loader.add_constructor`` (e.g. for ``!include``/``!load``
    tags) — PyYAML calls constructors as ``constructor(loader, node)``,
    which this method recognizes and routes accordingly.
    """
    if (
        len(args) == 2
        and _yaml is not None
        and isinstance(args[0], _yaml.constructor.BaseConstructor)
    ):
        return self._yaml_tag_constructor(*args, **kwds)

__subclasses__(*, recursive=False) classmethod

Return direct (or, if recursive, all transitive) subclasses.

Overrides :meth:type.__subclasses__ to add the recursive flag, which :meth:get_class_by_name and :meth:get_class_by_path use to discover every registered backend regardless of how deep its class hierarchy is.

Source code in src/yaconfiglib/backends/base.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
@classmethod
def __subclasses__(cls, *, recursive=False) -> list[type[_ty.Self]]:
    """Return direct (or, if *recursive*, all transitive) subclasses.

    Overrides :meth:`type.__subclasses__` to add the *recursive* flag,
    which :meth:`get_class_by_name` and :meth:`get_class_by_path` use
    to discover every registered backend regardless of how deep its
    class hierarchy is.
    """
    direct: list[type[_ty.Self]] = type.__subclasses__(cls)
    if not recursive:
        return direct
    # Deterministic, definition-order walk (depth-first, deduplicated).
    # The previous implementation returned a set, which made
    # get_class_by_path()'s "first match wins" depend on hash order —
    # backend resolution could differ between runs when two backends'
    # regexes both matched a path.
    ordered: list[type[_ty.Self]] = []
    for scls in direct:
        if scls not in ordered:
            ordered.append(scls)
        for nested in scls.__subclasses__(recursive=True):
            if nested not in ordered:
                ordered.append(nested)
    return ordered

can_load_path(path) classmethod

Return True if this backend's :attr:PATHNAME_REGEX matches path's filename.

Source code in src/yaconfiglib/backends/base.py
176
177
178
179
180
181
182
183
@classmethod
def can_load_path(cls, path: _Path) -> bool:
    """Return True if this backend's :attr:`PATHNAME_REGEX` matches *path*'s filename."""
    return (
        cls.PATHNAME_REGEX.match(path.name) is not None
        if cls.PATHNAME_REGEX
        else False
    )

dumps(data, **options)

Serialize data back to this backend's text format.

Optional — only needed for backends used with :func:yaconfiglib.dump/:func:yaconfiglib.dumps. Raises :class:NotImplementedError by default.

Source code in src/yaconfiglib/backends/base.py
123
124
125
126
127
128
129
130
def dumps(self, data: str, **options) -> str:
    """Serialize *data* back to this backend's text format.

    Optional — only needed for backends used with
    :func:`yaconfiglib.dump`/:func:`yaconfiglib.dumps`. Raises
    :class:`NotImplementedError` by default.
    """
    raise NotImplementedError

get_class_by_name(name) classmethod

Look up a registered backend class by its :attr:NAME.

Falls back to a derived name (lowercased class name with a trailing Loader/Config suffix stripped) for backends that don't set :attr:NAME explicitly. Used when a caller passes loader="yaml" (or similar) instead of a backend instance.

Source code in src/yaconfiglib/backends/base.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
@classmethod
def get_class_by_name(cls, name: str) -> type[_ty.Self]:
    """Look up a registered backend class by its :attr:`NAME`.

    Falls back to a derived name (lowercased class name with a
    trailing ``Loader``/``Config`` suffix stripped) for backends that
    don't set :attr:`NAME` explicitly. Used when a caller passes
    ``loader="yaml"`` (or similar) instead of a backend instance.
    """
    for scls in cls.__subclasses__(recursive=True):
        _name = getattr(scls, "NAME", None)
        if not _name:
            _name = (
                scls.__name__.lower().removesuffix("loader").removesuffix("config")
            )
        if _name == name:
            return scls

get_class_by_path(path) classmethod

Find the first registered backend class whose :meth:can_load_path matches path.

Raises:

Type Description
NotImplementedError

If no registered backend claims path.

Source code in src/yaconfiglib/backends/base.py
185
186
187
188
189
190
191
192
193
194
195
@classmethod
def get_class_by_path(cls, path: _Path):
    """Find the first registered backend class whose :meth:`can_load_path` matches *path*.

    Raises:
        NotImplementedError: If no registered backend claims *path*.
    """
    for scls in cls.__subclasses__(recursive=True):
        if scls.can_load_path(path):
            return scls
    raise NotImplementedError(f"Not reader for {path}")

load(path, **options)

Read path and return the parsed configuration object.

Subclasses must override this. Implementations typically accept additional keyword-only options specific to their format (e.g. encoding); unrecognized options should generally be ignored via **options rather than raising, since :class:~yaconfiglib.loader.ConfigLoader forwards a shared set of options to every backend it invokes.

Source code in src/yaconfiglib/backends/base.py
103
104
105
106
107
108
109
110
111
112
def load(self, path: _Path, **options) -> object:
    """Read *path* and return the parsed configuration object.

    Subclasses must override this. Implementations typically accept
    additional keyword-only options specific to their format (e.g.
    ``encoding``); unrecognized options should generally be ignored via
    ``**options`` rather than raising, since :class:`~yaconfiglib.loader.ConfigLoader`
    forwards a shared set of options to every backend it invokes.
    """
    raise NotImplementedError()

load_all(path, **options)

Yield one or more parsed documents from path.

The default implementation yields a single document produced by :meth:load. Backends that support multi-document sources (e.g. a directory or a multi-document YAML stream) should override this.

Source code in src/yaconfiglib/backends/base.py
114
115
116
117
118
119
120
121
def load_all(self, path: _Path, **options) -> _ty.Iterable[object]:
    """Yield one or more parsed documents from *path*.

    The default implementation yields a single document produced by
    :meth:`load`. Backends that support multi-document sources (e.g. a
    directory or a multi-document YAML stream) should override this.
    """
    yield self.load(path, **options)

YAML

yaconfiglib.backends.yaml.YamlConfig

Bases: ConfigBackend

Backend for *.yaml/*.yml files.

Automatically registers !include and !load tag constructors on the active PyYAML loader class so nested configuration files can be pulled in directly from YAML, e.g. database: !include "db.toml". Registration happens once per loader class and only when a parent :class:~yaconfiglib.loader.ConfigLoader is supplied via loader=.

DEFAULT_DUMPER_CLS = yaml.Dumper class-attribute instance-attribute

DEFAULT_LOADER_CLS = yaml.SafeLoader class-attribute instance-attribute

PATHNAME_REGEX = re.compile('.*\\.((yaml)|(yml))$', re.IGNORECASE) class-attribute instance-attribute

dumps(data, dumper_cls=None, **options)

Serialize data to a YAML string using dumper_cls (defaults to :attr:DEFAULT_DUMPER_CLS).

Source code in src/yaconfiglib/backends/yaml.py
165
166
167
168
def dumps(self, data: str, dumper_cls: yaml.Dumper = None, **options) -> str:
    """Serialize *data* to a YAML string using *dumper_cls* (defaults to :attr:`DEFAULT_DUMPER_CLS`)."""
    options.setdefault("Dumper", dumper_cls or self.DEFAULT_DUMPER_CLS)
    return yaml.dump(data, **options)

load(path, encoding=None, master=None, loader_cls=None, path_factory=None, loader=None, **options)

Parse path as YAML and return the resulting object.

Parameters:

Name Type Description Default
path Path | str

File to parse, either a Path or a string (converted via path_factory).

required
encoding str

Text encoding, defaults to :attr:DEFAULT_ENCODING.

None
master Loader

An in-progress PyYAML loader instance to inherit anchors/aliases from — used when this call originates from a !include/!load tag within another YAML document.

None
loader_cls type[Loader]

PyYAML loader class to use. Defaults to master's class if given, else :attr:DEFAULT_LOADER_CLS.

None
path_factory type[Path]

Path constructor used when path is a string.

None
loader ConfigBackend

The parent :class:~yaconfiglib.loader.ConfigLoader. When supplied, !include/!load tags are registered on loader_cls so nested includes resolve through it.

None

Returns:

Type Description
object

The parsed YAML document (typically a dict, list, or

object

scalar).

Source code in src/yaconfiglib/backends/yaml.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def load(
    self,
    path: Path | str,
    encoding: str = None,
    master: yaml.Loader = None,
    loader_cls: type[yaml.Loader] = None,
    path_factory: type[Path] = None,
    loader: ConfigBackend = None,
    **options,
) -> object:
    """Parse *path* as YAML and return the resulting object.

    Args:
        path: File to parse, either a ``Path`` or a string (converted
            via *path_factory*).
        encoding: Text encoding, defaults to :attr:`DEFAULT_ENCODING`.
        master: An in-progress PyYAML loader instance to inherit
            anchors/aliases from — used when this call originates from
            a ``!include``/``!load`` tag within another YAML document.
        loader_cls: PyYAML loader class to use. Defaults to *master*'s
            class if given, else :attr:`DEFAULT_LOADER_CLS`.
        path_factory: Path constructor used when *path* is a string.
        loader: The parent :class:`~yaconfiglib.loader.ConfigLoader`.
            When supplied, ``!include``/``!load`` tags are registered
            on *loader_cls* so nested includes resolve through it.

    Returns:
        The parsed YAML document (typically a ``dict``, ``list``, or
        scalar).
    """
    encoding = encoding or self.DEFAULT_ENCODING

    if path_factory is None:
        path_factory = self.DEFAULT_PATH_FACTORY
    if isinstance(path, str):
        path = path_factory(path)
    if master and not loader_cls:
        loader_cls = type(master)
    if loader_cls is None:
        loader_cls = self.DEFAULT_LOADER_CLS

    # Auto-register !include / !load tags if a loader is provided
    # and the tags haven't already been registered on this loader class.
    if loader is not None:
        self._register_include_tags(loader_cls, loader, path_factory)

    loader_instance = loader_cls(path.read_text(encoding=encoding))
    # Make the driving ConfigLoader reachable from the include constructor
    # (see _register_include_tags._construct) so nested !include/!load
    # resolve through THIS loader's settings, not the first one registered.
    if loader is not None:
        loader_instance._yaconfiglib_config_loader = loader
    try:
        if master:
            loader_instance.anchors = master.anchors
        data = loader_instance.get_single_data()
        return data
    finally:
        loader_instance.dispose()

TOML

yaconfiglib.backends.toml.TomlConfig

Bases: ConfigBackend

Backend for *.toml files.

Uses the standard library :mod:tomllib on Python 3.11+, falling back to the third-party toml package on older interpreters.

PATHNAME_REGEX = re.compile('.*\\.toml$', re.IGNORECASE) class-attribute instance-attribute

load(path, encoding, **kwargs)

Parse path as TOML and return the resulting dict.

Source code in src/yaconfiglib/backends/toml.py
27
28
29
def load(self, path: Path, encoding: str, **kwargs):
    """Parse *path* as TOML and return the resulting dict."""
    return toml.loads(path.read_text(encoding=encoding or self.DEFAULT_ENCODING))

JSON

yaconfiglib.backends.json.JsonConfig

Bases: ConfigBackend

Backend for *.json files, parsed via the standard library :mod:json module.

PATHNAME_REGEX = re.compile('.*\\.json$', re.IGNORECASE) class-attribute instance-attribute

dumps(data, **options)

Serialize data to a JSON string via :func:json.dumps.

Source code in src/yaconfiglib/backends/json.py
40
41
42
def dumps(self, data: str, **options) -> str:
    """Serialize *data* to a JSON string via :func:`json.dumps`."""
    return json.dumps(data, **options)

load(path, encoding=None, json_decoder_options=None, **options)

Parse path as JSON and return the resulting object.

Parameters:

Name Type Description Default
path Path

File to parse.

required
encoding str

Text encoding, defaults to :attr:DEFAULT_ENCODING.

None
json_decoder_options dict

Extra keyword arguments forwarded to :func:json.loads (e.g. object_hook, parse_float).

None
Source code in src/yaconfiglib/backends/json.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def load(
    self,
    path: Path,
    encoding: str = None,
    json_decoder_options: dict = None,
    **options,
) -> object:
    """Parse *path* as JSON and return the resulting object.

    Args:
        path: File to parse.
        encoding: Text encoding, defaults to :attr:`DEFAULT_ENCODING`.
        json_decoder_options: Extra keyword arguments forwarded to
            :func:`json.loads` (e.g. ``object_hook``, ``parse_float``).
    """
    encoding = encoding or self.DEFAULT_ENCODING

    return json.loads(
        path.read_text(encoding=encoding), **(json_decoder_options or {})
    )

INI

yaconfiglib.backends.ini.IniConfig

Bases: ConfigBackend

Backend for *.ini files, parsed via the standard library's :class:configparser.ConfigParser.

Returns a {section_name: {key: value}} mapping. All values are strings, matching :mod:configparser semantics — use interpolation (:func:yaconfiglib.utils.jinja2.interpolate) or manual coercion if typed values are needed.

DEFAULT_SECTION = 'DEFAULT' class-attribute instance-attribute

PATHNAME_REGEX = re.compile('.*\\.ini$', re.IGNORECASE) class-attribute instance-attribute

load(path, encoding=None, **options)

Parse path as INI and return a {section: {key: value}} dict.

Parameters:

Name Type Description Default
path Path

File to parse.

required
encoding str

Text encoding, defaults to :attr:DEFAULT_ENCODING.

None
**options object

Accepts ini_default_section — the section name used for :class:~configparser.ConfigParser's default_section, defaults to :attr:DEFAULT_SECTION.

{}
Source code in src/yaconfiglib/backends/ini.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def load(
    self,
    path: Path,
    encoding: str = None,
    **options: object,
) -> object:
    """Parse *path* as INI and return a ``{section: {key: value}}`` dict.

    Args:
        path: File to parse.
        encoding: Text encoding, defaults to :attr:`DEFAULT_ENCODING`.
        **options: Accepts ``ini_default_section`` — the section name
            used for :class:`~configparser.ConfigParser`'s
            ``default_section``, defaults to :attr:`DEFAULT_SECTION`.
    """
    encoding = encoding or self.DEFAULT_ENCODING

    parser_args = dict(
        default_section=options.setdefault(
            "ini_default_section", self.DEFAULT_SECTION
        )
    )

    parser = ConfigParser(**parser_args)
    parser.read_string(path.read_text(encoding=encoding), path.name)
    result = {}
    for section in parser.sections():
        d = result[section] = {}
        section_obj = parser[section]
        for key in section_obj:
            d[key] = section_obj[key]
    return result

dotenv

yaconfiglib.backends.dotenv.DotenvBackend(lowercase=True)

Bases: ConfigBackend

Parses .env files into a flat {KEY: value} mapping.

Supports: * KEY=value and export KEY=value syntax * Single- and double-quoted values * # comment lines and inline comments (outside quoted values)

Source code in src/yaconfiglib/backends/dotenv.py
66
67
def __init__(self, lowercase: bool = True) -> None:
    self.lowercase = lowercase

NAME = 'dotenv' class-attribute instance-attribute

PATHNAME_REGEX = re.compile('.*\\.env(\\..+)?$', re.IGNORECASE) class-attribute instance-attribute

lowercase = lowercase instance-attribute

load(path, encoding=None, path_factory=None, lowercase=None, **_options)

Parse path as a .env file into a flat {key: value} dict.

Parameters:

Name Type Description Default
path Path | str

File to parse, either a Path or a string (converted via path_factory).

required
encoding str

Text encoding, defaults to :attr:DEFAULT_ENCODING.

None
path_factory Callable[[str], Path]

Path constructor used when path is a string.

None
lowercase bool | None

Overrides the instance's lowercase for this call.

None

Returns:

Type Description
dict[str, str]

A flat mapping of variable name to string value. Values are

dict[str, str]

not coerced to other types — quoting is stripped but the

dict[str, str]

result stays all-string, matching dotenv conventions.

Source code in src/yaconfiglib/backends/dotenv.py
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def load(
    self,
    path: _Path | str,
    encoding: str = None,
    path_factory: _ty.Callable[[str], _Path] = None,
    lowercase: bool | None = None,
    **_options,
) -> dict[str, str]:
    """Parse *path* as a ``.env`` file into a flat ``{key: value}`` dict.

    Args:
        path: File to parse, either a ``Path`` or a string (converted
            via *path_factory*).
        encoding: Text encoding, defaults to :attr:`DEFAULT_ENCODING`.
        path_factory: Path constructor used when *path* is a string.
        lowercase: Overrides the instance's *lowercase* for this call.

    Returns:
        A flat mapping of variable name to string value. Values are
        not coerced to other types — quoting is stripped but the
        result stays all-string, matching dotenv conventions.
    """
    encoding = encoding or self.DEFAULT_ENCODING
    lowercase = self.lowercase if lowercase is None else lowercase
    if path_factory and not isinstance(path, _Path):
        path = path_factory(path)
    elif isinstance(path, str):
        path = (path_factory or self.DEFAULT_PATH_FACTORY)(path)

    result: dict[str, str] = {}
    for line in path.read_text(encoding=encoding).splitlines():
        # Skip blanks and comments
        if not line.strip() or _COMMENT_RE.match(line):
            continue
        line = _EXPORT_RE.sub("", line)
        m = _PAIR_RE.match(line)
        if m:
            key = m.group(1)
            if lowercase:
                key = key.lower()
            result[key] = _parse_value(m.group(2))
    return result

Environment variables

yaconfiglib.backends.env.EnvVarBackend(prefix='', lowercase=True, nested_delimiter=None, coerce=False)

Bases: ConfigBackend

Exposes os.environ (or a subset) as a configuration document.

Parameters

prefix: When given, only variables whose names start with prefix are included. The prefix is stripped from the key names. lowercase: If True (default) convert key names to lowercase for consistency with YAML/TOML conventions.

Source code in src/yaconfiglib/backends/env.py
73
74
75
76
77
78
79
80
81
82
83
def __init__(
    self,
    prefix: str = "",
    lowercase: bool = True,
    nested_delimiter: str | None = None,
    coerce: bool = False,
) -> None:
    self.prefix = prefix
    self.lowercase = lowercase
    self.nested_delimiter = nested_delimiter
    self.coerce = coerce

NAME = 'env' class-attribute instance-attribute

PATHNAME_REGEX = None class-attribute instance-attribute

coerce = coerce instance-attribute

lowercase = lowercase instance-attribute

nested_delimiter = nested_delimiter instance-attribute

prefix = prefix instance-attribute

load(path=None, prefix=None, lowercase=None, nested_delimiter=None, coerce=None, **_options)

Snapshot os.environ (optionally filtered/coerced) into a dict.

Parameters:

Name Type Description Default
path Path | str | None

Ignored — present only so this backend satisfies the :meth:~yaconfiglib.backends.base.ConfigBackend.load signature when invoked generically.

None
prefix str | None

Overrides the instance's prefix for this call.

None
lowercase bool | None

Overrides the instance's lowercase for this call.

None
nested_delimiter str | None

Overrides the instance's nested_delimiter. When set, keys containing the delimiter (after prefix stripping) are split into nested dicts, e.g. with delimiter "__", DB__PORT=5432 becomes {"db": {"port": 5432}}.

None
coerce bool | None

Overrides the instance's coerce. When True, string values are converted to None/bool/int/ float/parsed JSON (for values starting with [ or {) where they match, otherwise left as strings.

None

Returns:

Type Description
dict[str, object]

A flat or nested dict depending on nested_delimiter.

Source code in src/yaconfiglib/backends/env.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def load(
    self,
    path: _Path | str | None = None,
    prefix: str | None = None,
    lowercase: bool | None = None,
    nested_delimiter: str | None = None,
    coerce: bool | None = None,
    **_options,
) -> dict[str, object]:
    """Snapshot ``os.environ`` (optionally filtered/coerced) into a dict.

    Args:
        path: Ignored — present only so this backend satisfies the
            :meth:`~yaconfiglib.backends.base.ConfigBackend.load`
            signature when invoked generically.
        prefix: Overrides the instance's *prefix* for this call.
        lowercase: Overrides the instance's *lowercase* for this call.
        nested_delimiter: Overrides the instance's *nested_delimiter*.
            When set, keys containing the delimiter (after prefix
            stripping) are split into nested dicts, e.g. with
            delimiter ``"__"``, ``DB__PORT=5432`` becomes
            ``{"db": {"port": 5432}}``.
        coerce: Overrides the instance's *coerce*. When True, string
            values are converted to ``None``/``bool``/``int``/
            ``float``/parsed JSON (for values starting with ``[`` or
            ``{``) where they match, otherwise left as strings.

    Returns:
        A flat or nested dict depending on *nested_delimiter*.
    """
    prefix = self.prefix if prefix is None else prefix
    lowercase = self.lowercase if lowercase is None else lowercase
    nested_delimiter = (
        self.nested_delimiter
        if nested_delimiter is None
        else nested_delimiter
    )
    coerce = self.coerce if coerce is None else coerce

    result: dict[str, object] = {}
    for key, value in os.environ.items():
        if prefix and not key.startswith(prefix):
            continue
        clean_key = key[len(prefix):]
        if lowercase:
            clean_key = clean_key.lower()
        parsed_value = _coerce_value(value) if coerce else value
        if nested_delimiter and nested_delimiter in clean_key:
            parts = [part for part in clean_key.split(nested_delimiter) if part]
            if parts:
                _set_nested(result, parts, parsed_value)
            continue
        result[clean_key] = parsed_value
    return result

Commands and scripts

yaconfiglib.backends.command.CommandBackend

Bases: ConfigBackend

Executes a script/command and parses stdout into a configuration object.

Sources are recognized either by a URI scheme prefix (exec://, cmd://, sh://, or a format-tagged variant like cmd+json://) or by a .sh/.bat/.ps1/.cmd file extension. The command is run through the shell and its stdout is parsed as configuration data — this makes it easy to source secrets or dynamic values from external tools, e.g. cmd+json://aws secretsmanager get-secret-value ....

Output format resolution, in priority order:

  1. An explicit format= argument.
  2. The +fmt suffix on the scheme (e.g. cmd+yaml://...).
  3. A #!fmt shebang line at the start of the command's stdout.
  4. Sniffing: try json, yaml, toml, dotenv, ini in turn.

If parsing fails and no format was requested, the raw stdout string is returned as a fallback rather than raising.

NAME = 'command' class-attribute instance-attribute

PATHNAME_REGEX = re.compile('^(exec|cmd|sh|exec\\+\\w+|cmd\\+\\w+)(://|:\\\\|:/|:).*|.*?\\.(sh|bat|ps1|cmd)$', re.IGNORECASE) class-attribute instance-attribute

can_load_path(path) classmethod

Return True if path matches a command scheme prefix or script extension.

Source code in src/yaconfiglib/backends/command.py
46
47
48
49
50
51
52
53
@classmethod
def can_load_path(cls, path: Path) -> bool:
    """Return True if *path* matches a command scheme prefix or script extension."""
    path_str = str(path)
    return (
        cls.PATHNAME_REGEX.match(path_str) is not None or
        (cls.PATHNAME_REGEX.match(path.name) is not None if cls.PATHNAME_REGEX else False)
    )

load(path, encoding=None, format=None, path_factory=None, **options)

Run the command encoded in path and parse its stdout.

Parameters:

Name Type Description Default
path Path | str

A scheme-prefixed command string (e.g. "cmd+json://echo {}"), a bare shell command, or a script file path.

required
encoding str

Codec used to decode the command's stdout/stderr (default utf-8; previously the locale codec, which mangled UTF-8 output on Windows).

None
format str | list[str]

Explicit output format, or a comma-separated/list of candidate formats to try in order. Overrides shebang detection and sniffing.

None
path_factory Callable[[str], Path]

Unused; accepted for interface consistency.

None

Returns:

Type Description
object

The parsed stdout, or the raw stripped stdout string if no

object

format could be determined and sniffing failed.

Raises:

Type Description
CalledProcessError

If the command exits non-zero.

ValueError

If an explicit format/shebang format is requested but the output cannot be parsed as that format, or output is empty while a format was requested.

Source code in src/yaconfiglib/backends/command.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def load(
    self,
    path: Path | str,
    encoding: str = None,
    format: str | list[str] = None,
    path_factory: typing.Callable[[str], Path] = None,
    **options,
) -> object:
    """Run the command encoded in *path* and parse its stdout.

    Args:
        path: A scheme-prefixed command string (e.g.
            ``"cmd+json://echo {}"``), a bare shell command, or a
            script file path.
        encoding: Codec used to decode the command's stdout/stderr
            (default ``utf-8``; previously the locale codec, which
            mangled UTF-8 output on Windows).
        format: Explicit output format, or a comma-separated/list of
            candidate formats to try in order. Overrides shebang
            detection and sniffing.
        path_factory: Unused; accepted for interface consistency.

    Returns:
        The parsed stdout, or the raw stripped stdout string if no
        format could be determined and sniffing failed.

    Raises:
        subprocess.CalledProcessError: If the command exits non-zero.
        ValueError: If an explicit *format*/shebang format is
            requested but the output cannot be parsed as that format,
            or output is empty while a format was requested.
    """
    path_str = str(path)
    explicit_format = format

    # 1. Parse inline command schemes using regex to handle normalized slashes
    m = re.match(
        r"^(exec|cmd|sh|exec\+\w+|cmd\+\w+)(://|:\\|:/|:)",
        path_str,
        re.IGNORECASE
    )
    if m:
        scheme = m.group(1)
        command = path_str[m.end():]
        if "+" in scheme:
            _, scheme_fmt = scheme.split("+", 1)
            if not explicit_format:
                explicit_format = scheme_fmt
    else:
        command = path_str

    # 2. Execute command. Decode output explicitly: text=True alone uses
    # the locale codec (cp1252 on Windows), which mangles UTF-8 output
    # from tools like secret managers. errors="replace" keeps the
    # format-sniffing path total instead of raising mid-decode.
    result = subprocess.run(
        command,
        shell=True,
        capture_output=True,
        encoding=encoding or "utf-8",
        errors="replace",
        check=True
    )
    output = result.stdout.strip()

    # 3. Parse shebang from output if present
    shebang_format = None
    if output.startswith("#!"):
        lines = output.split("\n", 1)
        first_line = lines[0].strip()
        shebang_cmd = first_line[2:].strip()
        if shebang_cmd:
            parts = shebang_cmd.split()
            last_part = parts[-1] if parts else ""
            shebang_format = (
                last_part.split("/")[-1].split("\\")[-1].lower().lstrip(".")
            )
        output = lines[1] if len(lines) > 1 else ""

    if not output:
        if explicit_format or shebang_format:
            raise ValueError(
                f"Command output is empty, cannot parse as {explicit_format or shebang_format}"
            )
        return ""

    # 4. Parse content using yaconfiglib.loads
    from yaconfiglib import loads

    # Strip loader/format argument to avoid infinite recursion
    loads_options = {
        k: v for k, v in options.items() if k not in ("loader", "format")
    }

    candidates = []
    if explicit_format:
        if isinstance(explicit_format, str):
            candidates = [f.strip() for f in explicit_format.split(",")]
        else:
            candidates = list(explicit_format)
    elif shebang_format:
        candidates = [shebang_format]
    else:
        candidates = ["json", "yaml", "toml", "dotenv", "ini"]

    for fmt in candidates:
        if fmt == "command":
            continue
        try:
            return loads(output, loader=fmt, **loads_options)
        except Exception:  # noqa: BLE001 - format sniffing must survive ANY parse error
            # If explicit_format or shebang_format failed and is the only candidate,
            # we want to propagate the error. Otherwise, continue.
            if len(candidates) == 1 and (explicit_format or shebang_format):
                raise
            continue

    if explicit_format or shebang_format:
        raise ValueError(f"Failed to parse command output as {candidates}")

    # Sniffing fallback: return the raw output if no candidate parses successfully
    return output

In-memory Python objects

yaconfiglib.backends.python_backend.PythonBackend(data=None)

Bases: ConfigBackend

Wraps a plain Python object (dict, list, etc.) as a config source.

Useful for injecting computed or in-memory configuration into a loader chain without writing a file::

loader.load(
    "base.yaml",
    PythonBackend({"override_key": "override_value"}),
)
Source code in src/yaconfiglib/backends/python_backend.py
32
33
def __init__(self, data: object = None) -> None:
    self._data = data

NAME = 'python' class-attribute instance-attribute

PATHNAME_REGEX = None class-attribute instance-attribute

load(path=None, **_options)

Return the wrapped object, ignoring any file I/O.

If constructed with data=..., that object is always returned. Otherwise path itself is returned as-is, letting this backend double as a passthrough for already-parsed data injected into a loader chain.

Source code in src/yaconfiglib/backends/python_backend.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def load(
    self,
    path: _Path | str | object = None,
    **_options,
) -> object:
    """Return the wrapped object, ignoring any file I/O.

    If constructed with ``data=...``, that object is always returned.
    Otherwise *path* itself is returned as-is, letting this backend
    double as a passthrough for already-parsed data injected into a
    loader chain.
    """
    # If called as a YAML tag constructor path will be a string/Path;
    # otherwise callers pass the data object directly via __init__.
    if self._data is not None:
        return self._data
    return path  # fallback: treat path as the data object itself

Jinja2-templated sources

yaconfiglib.backends.jinja2.Jinja2ConfigLoader

Bases: ConfigBackend

Backend for *.j2/*.jinja2 template sources.

Renders the file as a Jinja2 template first, then dispatches the rendered text to whichever backend matches the un-templated filename (e.g. settings.yaml.j2 renders through Jinja2 and is then parsed as YAML). This lets any existing format be templated by simply appending a .j2/.jinja2 suffix, without needing a dedicated templated variant of each backend.

The rendered output is written to an in-memory path (MemPath, or a real temp file when pathlib_next is unavailable) before being handed to the resolved backend, so downstream backends see ordinary file content and don't need any Jinja2-specific handling.

NAME = 'jinja2' class-attribute instance-attribute

PATHNAME_REGEX = re.compile('.*\\.((j2)|(jinja2))$', re.IGNORECASE) class-attribute instance-attribute

load(path, encoding=None, loader=None, environment=None, **kwargs)

Render path as a Jinja2 template, then load the result with the matching backend.

Parameters:

Name Type Description Default
path Path

Path to the .j2/.jinja2 template file.

required
encoding str

Text encoding for reading the template and writing the rendered output. Defaults to :attr:DEFAULT_ENCODING.

None
loader ConfigBackend

The parent :class:~yaconfiglib.loader.ConfigLoader, forwarded to the resolved backend so nested !include/!load directives keep working.

None
environment Environment

A :class:jinja2.Environment to render with. Defaults to :data:yaconfiglib.utils.jinja2.DEFAULT_ENV. The legacy keyword envoriment (a historical typo) is still accepted as a fallback for backward compatibility — prefer environment.

None

Returns:

Type Description
None

The parsed object produced by the backend matching the

None

rendered filename (with the .j2/.jinja2 suffix

None

stripped).

Source code in src/yaconfiglib/backends/jinja2.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def load(
    self,
    path: Path,
    encoding: str = None,
    loader: ConfigBackend = None,
    environment: Environment = None,
    **kwargs,
) -> None:
    """Render *path* as a Jinja2 template, then load the result with the matching backend.

    Args:
        path: Path to the ``.j2``/``.jinja2`` template file.
        encoding: Text encoding for reading the template and writing
            the rendered output. Defaults to :attr:`DEFAULT_ENCODING`.
        loader: The parent :class:`~yaconfiglib.loader.ConfigLoader`,
            forwarded to the resolved backend so nested
            ``!include``/``!load`` directives keep working.
        environment: A :class:`jinja2.Environment` to render with.
            Defaults to :data:`yaconfiglib.utils.jinja2.DEFAULT_ENV`.
            The legacy keyword ``envoriment`` (a historical typo) is
            still accepted as a fallback for backward compatibility —
            prefer ``environment``.

    Returns:
        The parsed object produced by the backend matching the
        rendered filename (with the ``.j2``/``.jinja2`` suffix
        stripped).
    """
    encoding = encoding or self.DEFAULT_ENCODING
    environment = environment or kwargs.pop("envoriment", None)
    template = jinja2.load_template(
        path.read_text(encoding=encoding),
        environment=environment or jinja2.DEFAULT_ENV,
    )
    pathname = PosixPathname(path.as_posix())
    rendered = template.render(pathname=pathname)
    mempath = MemPath(
        path.with_name(path.stem).as_posix(),
    )
    mempath.parent.mkdir(parents=True, exist_ok=True)
    mempath.write_text(rendered, encoding=encoding)
    parent_loader = loader
    rendered_loader = ConfigBackend.get_class_by_path(mempath)()

    rendered = rendered_loader.load(
        mempath,
        encoding=encoding,
        loader=parent_loader,
        **kwargs,
    )
    return rendered